Skip to content

Commit 91641d2

Browse files
committed
add log injection remediation
1 parent f69332a commit 91641d2

File tree

9 files changed

+544
-2
lines changed

9 files changed

+544
-2
lines changed

core-codemods/src/main/java/io/codemodder/codemods/DefaultCodemods.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public static List<Class<? extends CodeChanger>> asList() {
3434
CodeQLJDBCResourceLeakCodemod.class,
3535
CodeQLJEXLInjectionCodemod.class,
3636
CodeQLJNDIInjectionCodemod.class,
37+
CodeQLLogInjectionCodemod.class,
3738
CodeQLMavenSecureURLCodemod.class,
3839
CodeQLOutputResourceLeakCodemod.class,
3940
CodeQLPredictableSeedCodemod.class,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package io.codemodder.codemods.codeql;
2+
3+
import com.contrastsecurity.sarif.Result;
4+
import com.github.javaparser.ast.CompilationUnit;
5+
import io.codemodder.*;
6+
import io.codemodder.codetf.DetectorRule;
7+
import io.codemodder.providers.sarif.codeql.ProvidedCodeQLScan;
8+
import io.codemodder.remediation.GenericRemediationMetadata;
9+
import io.codemodder.remediation.Remediator;
10+
import io.codemodder.remediation.loginjection.LogInjectionRemediator;
11+
import java.util.Optional;
12+
import javax.inject.Inject;
13+
14+
/** A codemod for automatically fixing Log Injection from CodeQL. */
15+
@Codemod(
16+
id = "codeql:java/log-injection",
17+
reviewGuidance = ReviewGuidance.MERGE_WITHOUT_REVIEW,
18+
importance = Importance.HIGH,
19+
executionPriority = CodemodExecutionPriority.HIGH)
20+
public final class CodeQLLogInjectionCodemod extends CodeQLRemediationCodemod {
21+
22+
private final Remediator<Result> remediator;
23+
24+
@Inject
25+
public CodeQLLogInjectionCodemod(
26+
@ProvidedCodeQLScan(ruleId = "java/log-injection") final RuleSarif sarif) {
27+
super(GenericRemediationMetadata.LOG_INJECTION.reporter(), sarif);
28+
this.remediator = new LogInjectionRemediator<>();
29+
}
30+
31+
@Override
32+
public DetectorRule detectorRule() {
33+
return new DetectorRule(
34+
"log-injection",
35+
"Log Injection",
36+
"https://codeql.github.com/codeql-query-help/java/java-log-injection/");
37+
}
38+
39+
@Override
40+
public CodemodFileScanningResult visit(
41+
final CodemodInvocationContext context, final CompilationUnit cu) {
42+
return remediator.remediateAll(
43+
cu,
44+
context.path().toString(),
45+
detectorRule(),
46+
ruleSarif.getResultsByLocationPath(context.path()),
47+
SarifFindingKeyUtil::buildFindingId,
48+
r -> r.getLocations().get(0).getPhysicalLocation().getRegion().getStartLine(),
49+
r ->
50+
Optional.ofNullable(
51+
r.getLocations().get(0).getPhysicalLocation().getRegion().getEndLine()),
52+
r ->
53+
Optional.ofNullable(
54+
r.getLocations().get(0).getPhysicalLocation().getRegion().getStartColumn()));
55+
}
56+
}

framework/codemodder-base/src/main/java/io/codemodder/remediation/GenericRemediationMetadata.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ public enum GenericRemediationMetadata {
1818
PREDICTABLE_SEED("predictable-seed"),
1919
ZIP_SLIP("zip-slip"),
2020
REGEX_INJECTION("regex-injection"),
21-
ERROR_MESSAGE_EXPOSURE("error-message-exposure");
21+
ERROR_MESSAGE_EXPOSURE("error-message-exposure"),
22+
LOG_INJECTION("log-injection");
2223

2324
private final CodemodReporterStrategy reporter;
2425

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.codemodder.remediation.loginjection;
2+
3+
import com.github.javaparser.ast.CompilationUnit;
4+
import io.codemodder.CodemodFileScanningResult;
5+
import io.codemodder.codetf.DetectorRule;
6+
import io.codemodder.remediation.*;
7+
import java.util.Collection;
8+
import java.util.Optional;
9+
import java.util.Set;
10+
import java.util.function.Function;
11+
12+
/** Remediator for Log Injection vulnerabilities. */
13+
public final class LogInjectionRemediator<T> implements Remediator<T> {
14+
15+
private final SearcherStrategyRemediator<T> searchStrategyRemediator;
16+
17+
public LogInjectionRemediator() {
18+
this.searchStrategyRemediator =
19+
new SearcherStrategyRemediator.Builder<T>()
20+
.withSearcherStrategyPair(
21+
new FixCandidateSearcher.Builder<T>()
22+
.withMatcher(
23+
n ->
24+
Optional.of(n)
25+
.map(MethodOrConstructor::new)
26+
.filter(mce -> mce.isMethodCallWithNameIn(loggerNames))
27+
.filter(mce -> mce.asNode().hasScope())
28+
.filter(mce -> !mce.getArguments().isEmpty())
29+
.isPresent())
30+
.build(),
31+
new LogStatementFixer())
32+
.build();
33+
}
34+
35+
@Override
36+
public CodemodFileScanningResult remediateAll(
37+
CompilationUnit cu,
38+
String path,
39+
DetectorRule detectorRule,
40+
Collection<T> findingsForPath,
41+
Function<T, String> findingIdExtractor,
42+
Function<T, Integer> findingStartLineExtractor,
43+
Function<T, Optional<Integer>> findingEndLineExtractor,
44+
Function<T, Optional<Integer>> findingStartColumnExtractor) {
45+
return searchStrategyRemediator.remediateAll(
46+
cu,
47+
path,
48+
detectorRule,
49+
findingsForPath,
50+
findingIdExtractor,
51+
findingStartLineExtractor,
52+
findingEndLineExtractor,
53+
findingStartColumnExtractor);
54+
}
55+
56+
private static final Set<String> loggerNames =
57+
Set.of("log", "warn", "error", "info", "debug", "trace", "fatal");
58+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package io.codemodder.remediation.loginjection;
2+
3+
import static io.codemodder.javaparser.JavaParserTransformer.wrap;
4+
5+
import com.github.javaparser.ast.CompilationUnit;
6+
import com.github.javaparser.ast.Node;
7+
import com.github.javaparser.ast.NodeList;
8+
import com.github.javaparser.ast.body.Parameter;
9+
import com.github.javaparser.ast.body.VariableDeclarator;
10+
import com.github.javaparser.ast.expr.BinaryExpr;
11+
import com.github.javaparser.ast.expr.Expression;
12+
import com.github.javaparser.ast.expr.MethodCallExpr;
13+
import com.github.javaparser.ast.expr.NameExpr;
14+
import com.github.javaparser.resolution.types.ResolvedType;
15+
import io.codemodder.DependencyGAV;
16+
import io.codemodder.ast.ASTs;
17+
import io.codemodder.ast.LocalDeclaration;
18+
import io.codemodder.remediation.RemediationStrategy;
19+
import io.codemodder.remediation.SuccessOrReason;
20+
import io.github.pixee.security.Newlines;
21+
import java.util.List;
22+
import java.util.Optional;
23+
24+
/**
25+
* The shapes of code we want to be able to fix:
26+
*
27+
* <pre>
28+
* log.info("User with id: " + userId + " has been created");
29+
* logger.error("User with id: " + userId + " has been created", ex);
30+
* log.warn(msg);
31+
* </pre>
32+
*/
33+
final class LogStatementFixer implements RemediationStrategy {
34+
35+
@Override
36+
public SuccessOrReason fix(final CompilationUnit compilationUnit, final Node node) {
37+
MethodCallExpr logCall = (MethodCallExpr) node;
38+
NodeList<Expression> arguments = logCall.getArguments();
39+
return fixArguments(arguments);
40+
}
41+
42+
private SuccessOrReason fixArguments(final NodeList<Expression> arguments) {
43+
44+
// first and only is NameExpr (not an exception) (args == 1), (args can be 2 if exception)
45+
if ((arguments.size() == 1 && arguments.get(0).isNameExpr())
46+
|| (arguments.size() == 2
47+
&& arguments.get(0).isNameExpr()
48+
&& isException(arguments.get(1)))) {
49+
NameExpr argument = arguments.get(0).asNameExpr();
50+
wrapWithNewlineSanitizer(argument);
51+
return SuccessOrReason.success(List.of(DependencyGAV.OWASP_XSS_JAVA_ENCODER));
52+
}
53+
54+
// first is string literal and second is NameExpr (args can be 3 if not exception)
55+
if ((arguments.size() == 2
56+
&& arguments.get(0).isStringLiteralExpr()
57+
&& arguments.get(1).isNameExpr())
58+
|| (arguments.size() == 3
59+
&& arguments.get(0).isStringLiteralExpr()
60+
&& arguments.get(1).isNameExpr()
61+
&& isException(arguments.get(2)))) {
62+
NameExpr argument = arguments.get(1).asNameExpr();
63+
wrapWithNewlineSanitizer(argument);
64+
return SuccessOrReason.success(List.of(DependencyGAV.OWASP_XSS_JAVA_ENCODER));
65+
}
66+
67+
// first is BinaryExpr with NameExpr in it (args == 2) (args can be 3 if last is exception)
68+
if ((arguments.size() == 2 && arguments.get(0).isBinaryExpr())
69+
|| (arguments.size() == 3
70+
&& arguments.get(0).isBinaryExpr()
71+
&& isException(arguments.get(2)))) {
72+
BinaryExpr binaryExpr = arguments.get(0).asBinaryExpr();
73+
Optional<Expression> expressionToWrap = findExpressionToWrap(binaryExpr);
74+
if (expressionToWrap.isPresent()) {
75+
wrapWithNewlineSanitizer(expressionToWrap.get());
76+
return SuccessOrReason.success(List.of(DependencyGAV.OWASP_XSS_JAVA_ENCODER));
77+
}
78+
}
79+
80+
return SuccessOrReason.reason("Unfixable log call shape");
81+
}
82+
83+
private boolean isException(final Expression expression) {
84+
if (expression.isNameExpr()) {
85+
try {
86+
ResolvedType type = expression.calculateResolvedType();
87+
String typeName = type.describe();
88+
return isExceptionTypeName(typeName);
89+
} catch (Exception e) {
90+
Optional<LocalDeclaration> declarationRef =
91+
ASTs.findEarliestLocalDeclarationOf(expression, "ex");
92+
if (declarationRef.isPresent()) {
93+
LocalDeclaration localDeclaration = declarationRef.get();
94+
Node declaration = localDeclaration.getDeclaration();
95+
// handle if its a parameter or a local variable
96+
if (declaration instanceof Parameter param) {
97+
String typeAsString = param.getTypeAsString();
98+
return isExceptionTypeName(typeAsString);
99+
} else if (declaration instanceof VariableDeclarator var) {
100+
String typeAsString = var.getTypeAsString();
101+
return isExceptionTypeName(typeAsString);
102+
}
103+
}
104+
Optional<Node> nameSourceNodeRef = ASTs.findNonCallableSimpleNameSource(expression, "e");
105+
if (nameSourceNodeRef.isPresent()) {
106+
Node declaration = nameSourceNodeRef.get();
107+
// handle if its a parameter or a local variable
108+
if (declaration instanceof Parameter param) {
109+
String typeAsString = param.getTypeAsString();
110+
return isExceptionTypeName(typeAsString);
111+
} else if (declaration instanceof VariableDeclarator var) {
112+
String typeAsString = var.getTypeAsString();
113+
return isExceptionTypeName(typeAsString);
114+
}
115+
}
116+
}
117+
}
118+
return false;
119+
}
120+
121+
private static boolean isExceptionTypeName(final String typeName) {
122+
return typeName.endsWith("Exception") || typeName.endsWith("Throwable");
123+
}
124+
125+
/**
126+
* We handle 4 expression code shapes. <code>
127+
* print(user.getName());
128+
* print("Hello, " + user.getName());
129+
* print(user.getName() + ", hello!");
130+
* print("Hello, " + user.getName() + ", hello!");
131+
* </code>
132+
*
133+
* <p>Note that we should only handle, for the tougher cases, string literals in combination with
134+
* the given expression. Note any other combination of expressions.
135+
*/
136+
private static Optional<Expression> findExpressionToWrap(final Expression argument) {
137+
138+
if (argument.isNameExpr()) {
139+
return Optional.of(argument);
140+
} else if (argument.isBinaryExpr()) {
141+
BinaryExpr binaryExpr = argument.asBinaryExpr();
142+
if (binaryExpr.getLeft().isBinaryExpr() && binaryExpr.getRight().isStringLiteralExpr()) {
143+
BinaryExpr leftBinaryExpr = binaryExpr.getLeft().asBinaryExpr();
144+
if (leftBinaryExpr.getLeft().isStringLiteralExpr()
145+
&& !leftBinaryExpr.getRight().isStringLiteralExpr()) {
146+
return Optional.of(leftBinaryExpr.getRight());
147+
}
148+
} else if (binaryExpr.getLeft().isStringLiteralExpr()
149+
&& binaryExpr.getRight().isStringLiteralExpr()) {
150+
return Optional.empty();
151+
} else if (binaryExpr.getLeft().isStringLiteralExpr()) {
152+
return Optional.of(binaryExpr.getRight());
153+
} else if (binaryExpr.getRight().isStringLiteralExpr()) {
154+
return Optional.of(binaryExpr.getLeft());
155+
}
156+
}
157+
return Optional.empty();
158+
}
159+
160+
private static void wrapWithNewlineSanitizer(final Expression expression) {
161+
wrap(expression).withStaticMethod(Newlines.class.getName(), "stripNewLines", true);
162+
}
163+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
This change ensures that log messages can't contain newline characters, leaving you vulnerable to Log Forging / Log Injection.
2+
3+
If malicious users can get newline characters into a log message, they can inject and forge new log entries that look like they came from the server, and trick log analysis tools, administrators, and more. This leads to vulnerabilities like Log Injection, Log Forging, and more attacks from there.
4+
5+
Our change simply strips out newline characters from log messages, ensuring that they can't be used to forge new log entries.
6+
```diff
7+
+ import io.github.pixee.security.Newlines;
8+
...
9+
String orderId = getUserOrderId();
10+
- log.info("User order ID: " + orderId);
11+
+ log.info("User order ID: " + Newlines.stripNewlines(orderId));
12+
```
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"summary" : "Introduced protections against Log Injection / Forging attacks",
3+
"change" : "Added a call to replace any newlines the value",
4+
"reviewGuidanceJustification" : "This strips newlines from the value before it is logged, preventing log injection attacks",
5+
"references" : ["https://owasp.org/www-community/attacks/Log_Injection", "https://knowledge-base.secureflag.com/vulnerabilities/inadequate_input_validation/log_injection_vulnerability.html", "https://cwe.mitre.org/data/definitions/117.html"]
6+
}

framework/codemodder-base/src/test/java/io/codemodder/remediation/jndiinjection/DefaultJNDIInjectionRemediatorTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ void it_doesnt_fix_unfixable(
5353
i -> Optional.empty());
5454

5555
assertThat(result.changes()).isEmpty();
56-
;
5756

5857
List<UnfixedFinding> unfixedFindings = result.unfixedFindings();
5958
assertThat(unfixedFindings).hasSize(1);

0 commit comments

Comments
 (0)