diff --git a/java/ql/src/experimental/Security/CWE/CWE-020/Log4jJndiInjection.java b/java/ql/src/experimental/Security/CWE/CWE-020/Log4jJndiInjection.java new file mode 100644 index 000000000000..23c4dc3bd461 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-020/Log4jJndiInjection.java @@ -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; + } +} diff --git a/java/ql/src/experimental/Security/CWE/CWE-020/Log4jJndiInjection.qhelp b/java/ql/src/experimental/Security/CWE/CWE-020/Log4jJndiInjection.qhelp new file mode 100644 index 000000000000..8d9ceb6008a1 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-020/Log4jJndiInjection.qhelp @@ -0,0 +1,52 @@ + + + + +

+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). +

+

+As per Apache's Log4j security guide: Apache Log4j2 <=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. +

+
+ + +

+This issue was remediated in Log4j v2.15.0. The Apache Logging Services team provides the following mitigation advice: +

+

+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). +

+

+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. +

+

+Where possible, upgrade to Log4j version 2.15.0. If you are using Log4j v1 there is a migration guide available. +

+

+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. +

+

+If upgrading is not possible, then ensure the -Dlog4j2.formatMsgNoLookups=true system property is set on both client- and server-side components. +

+
+ + +

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

+ +
+ + +
  • GitHub Advisory Database: Remote code injection in Log4j.
  • +
    +
    \ No newline at end of file diff --git a/java/ql/src/experimental/Security/CWE/CWE-020/Log4jJndiInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-020/Log4jJndiInjection.ql new file mode 100644 index 000000000000..a6005d97c071 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-020/Log4jJndiInjection.ql @@ -0,0 +1,182 @@ +/** + * @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 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.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"