Skip to content

Commit 63831cc

Browse files
authored
Merge pull request github#5099 from porcupineyhairs/javaLogInjection
Java : Add Log Injection Vulnerability
2 parents b023d73 + a88c368 commit 63831cc

File tree

6 files changed

+175
-27
lines changed

6 files changed

+175
-27
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
2+
<!DOCTYPE qhelp PUBLIC
3+
"-//Semmle//qhelp//EN"
4+
"qhelp.dtd">
5+
<qhelp>
6+
7+
<overview>
8+
9+
<p>If unsanitized user input is written to a log entry, a malicious user may be able to forge new log entries.</p>
10+
11+
<p>Forgery can occur if a user provides some input creating the appearance of multiple
12+
log entries. This can include unescaped new-line characters, or HTML or other markup.</p>
13+
</overview>
14+
15+
<recommendation>
16+
<p>
17+
User input should be suitably sanitized before it is logged.
18+
</p>
19+
<p>
20+
If the log entries are plain text then line breaks should be removed from user input, using for example
21+
<code>String replace(char oldChar, char newChar)</code> or similar. Care should also be taken that user input is clearly marked
22+
in log entries, and that a malicious user cannot cause confusion in other ways.
23+
</p>
24+
<p>
25+
For log entries that will be displayed in HTML, user input should be HTML encoded before being logged, to prevent forgery and
26+
other forms of HTML injection.
27+
</p>
28+
29+
</recommendation>
30+
31+
<example>
32+
<p>In the example, a username, provided by the user, is logged using <code>logger.warn</code> (from <code>org.slf4j.Logger</code>).
33+
In the first case (<code>/bad</code> endpoint), the username is logged without any sanitization.
34+
If a malicious user provides <code>Guest'%0AUser:'Admin</code> as a username parameter,
35+
the log entry will be split into two separate lines, where the first line will be <code>User:'Guest'</code> and the second one will be <code>User:'Admin'</code>.
36+
</p>
37+
<sample src="LogInjectionBad.java" />
38+
39+
<p> In the second case (<code>/good</code> endpoint), <code>matches()</code> is used to ensure the user input only has alphanumeric characters.
40+
If a malicious user provides `Guest'%0AUser:'Admin` as a username parameter,
41+
the log entry will not be split into two separate lines, resulting in a single line <code>User:'Guest'User:'Admin'</code>.</p>
42+
43+
<sample src="LogInjectionGood.java" />
44+
</example>
45+
46+
<references>
47+
<li>OWASP: <a href="https://owasp.org/www-community/attacks/Log_Injection">Log Injection</a>.</li>
48+
</references>
49+
</qhelp>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @name Log Injection
3+
* @description Building log entries from user-controlled data is vulnerable to
4+
* insertion of forged log entries by a malicious user.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @precision high
8+
* @id java/log-injection
9+
* @tags security
10+
* external/cwe/cwe-117
11+
*/
12+
13+
import java
14+
import DataFlow::PathGraph
15+
import experimental.semmle.code.java.Logging
16+
import semmle.code.java.dataflow.FlowSources
17+
18+
/**
19+
* A taint-tracking configuration for tracking untrusted user input used in log entries.
20+
*/
21+
private class LogInjectionConfiguration extends TaintTracking::Configuration {
22+
LogInjectionConfiguration() { this = "Log Injection" }
23+
24+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
25+
26+
override predicate isSink(DataFlow::Node sink) {
27+
sink.asExpr() = any(LoggingCall c).getALogArgument()
28+
}
29+
30+
override predicate isSanitizer(DataFlow::Node node) {
31+
node.getType() instanceof BoxedType or node.getType() instanceof PrimitiveType
32+
}
33+
}
34+
35+
from LogInjectionConfiguration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
36+
where cfg.hasFlowPath(source, sink)
37+
select sink.getNode(), source, sink, "$@ flows to log entry.", source.getNode(),
38+
"User-provided value"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.example.restservice;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.RequestParam;
7+
import org.springframework.web.bind.annotation.RestController;
8+
9+
@RestController
10+
public class LogInjection {
11+
12+
private final Logger log = LoggerFactory.getLogger(LogInjection.class);
13+
14+
// /bad?username=Guest'%0AUser:'Admin
15+
@GetMapping("/bad")
16+
public String bad(@RequestParam(value = "username", defaultValue = "name") String username) {
17+
log.warn("User:'{}'", username);
18+
// The logging call above would result in multiple log entries as shown below:
19+
// User:'Guest'
20+
// User:'Admin'
21+
return username;
22+
}
23+
}
24+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.example.restservice;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.RequestParam;
7+
import org.springframework.web.bind.annotation.RestController;
8+
9+
@RestController
10+
public class LogInjection {
11+
12+
private final Logger log = LoggerFactory.getLogger(LogInjection.class);
13+
14+
// /good?username=Guest'%0AUser:'Admin
15+
@GetMapping("/good")
16+
public String good(@RequestParam(value = "username", defaultValue = "name") String username) {
17+
// The regex check here, allows only alphanumeric characters to pass.
18+
// Hence, does not result in log injection
19+
if (username.matches("\w*")) {
20+
log.warn("User:'{}'", username);
21+
22+
return username;
23+
}
24+
}
25+
}

java/ql/src/experimental/Security/CWE/CWE-532/SensitiveInfoLog.ql

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java
1111
import semmle.code.java.dataflow.TaintTracking
1212
import semmle.code.java.security.SensitiveActions
13+
import experimental.semmle.code.java.Logging
1314
import DataFlow
1415
import PathGraph
1516

@@ -27,38 +28,14 @@ class CredentialExpr extends Expr {
2728
}
2829
}
2930

30-
/** Class of popular logging utilities * */
31-
class LoggerType extends RefType {
32-
LoggerType() {
33-
this.hasQualifiedName("org.apache.log4j", "Category") or //Log4J
34-
this.hasQualifiedName("org.apache.logging.log4j", "Logger") or //Log4J 2
35-
this.hasQualifiedName("org.slf4j", "Logger") or //SLF4j and Gradle Logging
36-
this.hasQualifiedName("org.jboss.logging", "BasicLogger") or //JBoss Logging
37-
this.hasQualifiedName("org.jboss.logging", "Logger") or //JBoss Logging (`org.jboss.logging.Logger` in some implementations like JBoss Application Server 4.0.4 did not implement `BasicLogger`)
38-
this.hasQualifiedName("org.apache.commons.logging", "Log") or //Apache Commons Logging
39-
this.hasQualifiedName("org.scijava.log", "Logger") //SciJava Logging
40-
}
41-
}
42-
43-
predicate isSensitiveLoggingSink(DataFlow::Node sink) {
44-
exists(MethodAccess ma |
45-
ma.getMethod().getDeclaringType() instanceof LoggerType and
46-
(
47-
ma.getMethod().hasName("debug") or
48-
ma.getMethod().hasName("trace") or
49-
ma.getMethod().hasName("debugf") or
50-
ma.getMethod().hasName("debugv")
51-
) and //Check low priority log levels which are more likely to be real issues to reduce false positives
52-
sink.asExpr() = ma.getAnArgument()
53-
)
54-
}
55-
5631
class LoggerConfiguration extends DataFlow::Configuration {
5732
LoggerConfiguration() { this = "Logger Configuration" }
5833

5934
override predicate isSource(DataFlow::Node source) { source.asExpr() instanceof CredentialExpr }
6035

61-
override predicate isSink(DataFlow::Node sink) { isSensitiveLoggingSink(sink) }
36+
override predicate isSink(DataFlow::Node sink) {
37+
exists(LoggingCall c | sink.asExpr() = c.getALogArgument())
38+
}
6239

6340
override predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
6441
TaintTracking::localTaintStep(node1, node2)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Provides classes and predicates for working with loggers.
3+
*/
4+
5+
import java
6+
7+
/** Models a call to a logging method. */
8+
class LoggingCall extends MethodAccess {
9+
LoggingCall() {
10+
exists(RefType t, Method m |
11+
t.hasQualifiedName("org.apache.log4j", "Category") or // Log4j 1
12+
t.hasQualifiedName("org.apache.logging.log4j", ["Logger", "LogBuilder"]) or // Log4j 2
13+
t.hasQualifiedName("org.apache.commons.logging", "Log") or
14+
// JBoss Logging (`org.jboss.logging.Logger` in some implementations like JBoss Application Server 4.0.4 did not implement `BasicLogger`)
15+
t.hasQualifiedName("org.jboss.logging", ["BasicLogger", "Logger"]) or
16+
t.hasQualifiedName("org.slf4j.spi", "LoggingEventBuilder") or
17+
t.hasQualifiedName("org.slf4j", "Logger") or
18+
t.hasQualifiedName("org.scijava.log", "Logger") or
19+
t.hasQualifiedName("com.google.common.flogger", "LoggingApi") or
20+
t.hasQualifiedName("java.lang", "System$Logger") or
21+
t.hasQualifiedName("java.util.logging", "Logger") or
22+
t.hasQualifiedName("android.util", "Log")
23+
|
24+
(
25+
m.getDeclaringType().getASourceSupertype*() = t or
26+
m.getDeclaringType().extendsOrImplements*(t)
27+
) and
28+
m.getReturnType() instanceof VoidType and
29+
this = m.getAReference()
30+
)
31+
}
32+
33+
/** Returns an argument which would be logged by this call. */
34+
Argument getALogArgument() { result = this.getArgument(_) }
35+
}

0 commit comments

Comments
 (0)