Skip to content

Commit 6a7eedf

Browse files
authored
Added path traversal remediation (#493)
The scope of this is limited for a first introduction, since path traversal will be a tricky one to generalize more. This change introduces a remediator that will sanitize PT flows that start with an obvious source of taint that is intended to be a filename -- multipart file names.
1 parent 0be1881 commit 6a7eedf

File tree

7 files changed

+267
-0
lines changed

7 files changed

+267
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public enum GenericRemediationMetadata {
2020
REGEX_INJECTION("regex-injection"),
2121
ERROR_MESSAGE_EXPOSURE("error-message-exposure"),
2222
LOG_INJECTION("log-injection"),
23+
PATH_TRAVERSAL("path-traversal"),
2324
WEAK_CRYPTO_ALGORITHM("weak-crypto-algorithm");
2425

2526
private final CodemodReporterStrategy reporter;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package io.codemodder.remediation.pathtraversal;
2+
3+
import com.github.javaparser.ast.CompilationUnit;
4+
import com.github.javaparser.ast.expr.MethodCallExpr;
5+
import io.codemodder.CodemodFileScanningResult;
6+
import io.codemodder.codetf.DetectorRule;
7+
import io.codemodder.remediation.FixCandidateSearcher;
8+
import io.codemodder.remediation.Remediator;
9+
import io.codemodder.remediation.SearcherStrategyRemediator;
10+
import java.util.Collection;
11+
import java.util.Optional;
12+
import java.util.function.Function;
13+
14+
/** Remediate path traversal vulns. */
15+
public final class PathTraversalRemediator<T> implements Remediator<T> {
16+
17+
private final SearcherStrategyRemediator<T> searchStrategyRemediator;
18+
19+
public PathTraversalRemediator() {
20+
this.searchStrategyRemediator =
21+
new SearcherStrategyRemediator.Builder<T>()
22+
.withSearcherStrategyPair(
23+
new FixCandidateSearcher.Builder<T>()
24+
.withMatcher(
25+
node ->
26+
Optional.of(node)
27+
.map(n -> n instanceof MethodCallExpr ? (MethodCallExpr) n : null)
28+
.filter(PathTraversalRemediator::isSpringMultipartFilenameCall)
29+
.isPresent())
30+
.build(),
31+
new SpringMultipartFixStrategy())
32+
.build();
33+
}
34+
35+
@Override
36+
public CodemodFileScanningResult remediateAll(
37+
final CompilationUnit cu,
38+
final String path,
39+
final DetectorRule detectorRule,
40+
final Collection<T> findingsForPath,
41+
final Function<T, String> findingIdExtractor,
42+
final Function<T, Integer> findingStartLineExtractor,
43+
final Function<T, Optional<Integer>> findingEndLineExtractor,
44+
final 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 boolean isSpringMultipartFilenameCall(final MethodCallExpr methodCallExpr) {
57+
return methodCallExpr.hasScope()
58+
&& "getOriginalFilename".equals(methodCallExpr.getNameAsString());
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package io.codemodder.remediation.pathtraversal;
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.expr.MethodCallExpr;
8+
import io.codemodder.remediation.RemediationStrategy;
9+
import io.codemodder.remediation.SuccessOrReason;
10+
import io.github.pixee.security.Filenames;
11+
12+
/**
13+
* Fix strategy for Spring MultipartFile getOriginalFilename() calls which wraps with
14+
* java-security-toolkit API for guaranteeing a simple file name.
15+
*/
16+
final class SpringMultipartFixStrategy implements RemediationStrategy {
17+
@Override
18+
public SuccessOrReason fix(final CompilationUnit cu, final Node node) {
19+
MethodCallExpr methodCallExpr = (MethodCallExpr) node;
20+
boolean success =
21+
wrap(methodCallExpr).withStaticMethod(Filenames.class.getName(), "toSimpleFileName", false);
22+
return success ? SuccessOrReason.success() : SuccessOrReason.reason("Wrap failed");
23+
}
24+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
This change attempts to remediate path traversal (also called directory traversal, local file include, etc.) vulnerabilities.
2+
3+
Our changes may introduce sanitization up front, if the input looks like it's a file name (like from a multipart HTTP request), or validation if it appears to be a piece of path.
4+
5+
6+
```diff
7+
+ import io.github.pixee.security.Filenames;
8+
9+
...
10+
11+
- String fileName = request.getFileName();
12+
+ String fileName = Filenames.toSimpleFileName(request.getFileName());
13+
```
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"summary" : "Remediate path traversal",
3+
"change": "Added a validator/sanitizer",
4+
"reviewGuidanceIJustification" : "These changes should be reviewed to ensure that the validator/sanitizer is correctly implemented and won't disrupt the application's functionality.",
5+
"references" : [
6+
"https://cwe.mitre.org/data/definitions/35.html"
7+
]
8+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package io.codemodder;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.github.javaparser.StaticJavaParser;
6+
import com.github.javaparser.ast.CompilationUnit;
7+
import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter;
8+
import io.codemodder.codetf.DetectorRule;
9+
import io.codemodder.remediation.Remediator;
10+
import java.util.List;
11+
import java.util.Objects;
12+
import java.util.Optional;
13+
import java.util.stream.Stream;
14+
import org.junit.jupiter.api.DynamicTest;
15+
import org.junit.jupiter.api.TestFactory;
16+
17+
/** A mixin which provides basic structure for testing remediators. */
18+
public interface RemediatorTestMixin<T> {
19+
20+
@TestFactory
21+
default Stream<DynamicTest> it_fixes_the_code() {
22+
Stream<FixableSample> fixableSamples = createFixableSamples();
23+
24+
return fixableSamples.map(
25+
sample -> {
26+
String beforeCode = sample.beforeCode();
27+
String afterCode = sample.afterCode();
28+
int line = sample.line();
29+
30+
return DynamicTest.dynamicTest(
31+
beforeCode,
32+
() -> {
33+
CompilationUnit cu = StaticJavaParser.parse(beforeCode);
34+
LexicalPreservingPrinter.setup(cu);
35+
Remediator<Object> remediator = createRemediator();
36+
37+
DetectorRule rule = new DetectorRule("rule-123", "my-remediation-rule", null);
38+
39+
CodemodFileScanningResult result =
40+
remediator.remediateAll(
41+
cu,
42+
"foo",
43+
rule,
44+
List.of(new Object()),
45+
f -> "123",
46+
f -> line,
47+
x -> Optional.empty(),
48+
x -> Optional.empty());
49+
assertThat(result.unfixedFindings()).isEmpty();
50+
assertThat(result.changes()).hasSize(1);
51+
CodemodChange change = result.changes().get(0);
52+
assertThat(change.lineNumber()).isEqualTo(line);
53+
54+
String actualCode = LexicalPreservingPrinter.print(cu);
55+
assertThat(actualCode).isEqualToIgnoringCase(afterCode);
56+
});
57+
});
58+
}
59+
60+
/** Build the remediator to test. */
61+
Remediator<Object> createRemediator();
62+
63+
/** Create samples to test. */
64+
Stream<FixableSample> createFixableSamples();
65+
66+
/** Create unfixable samples. */
67+
Stream<UnfixableSample> createUnfixableSamples();
68+
69+
/** Represents a finding that can be fixed. */
70+
record FixableSample(String beforeCode, String afterCode, int line) {
71+
public FixableSample {
72+
Objects.requireNonNull(beforeCode);
73+
Objects.requireNonNull(afterCode);
74+
if (line < 0) {
75+
throw new IllegalArgumentException("Line number must be non-negative");
76+
}
77+
}
78+
}
79+
80+
/** Represents a finding that can't be fixed for some reason. */
81+
record UnfixableSample(String code, int line) {
82+
public UnfixableSample {
83+
Objects.requireNonNull(code);
84+
if (line < 0) {
85+
throw new IllegalArgumentException("Line number must be non-negative");
86+
}
87+
}
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.codemodder.remediation.pathtraversal;
2+
3+
import io.codemodder.RemediatorTestMixin;
4+
import io.codemodder.remediation.Remediator;
5+
import java.util.stream.Stream;
6+
7+
final class PathTraversalRemediatorTest implements RemediatorTestMixin<Object> {
8+
9+
@Override
10+
public Remediator<Object> createRemediator() {
11+
return new PathTraversalRemediator<>();
12+
}
13+
14+
@Override
15+
public Stream<FixableSample> createFixableSamples() {
16+
return Stream.of(
17+
new FixableSample(
18+
"""
19+
import org.springframework.web.multipart.MultipartFile;
20+
public class Test {
21+
public void test(MultipartFile file) {
22+
String filename = file.getOriginalFilename();
23+
}
24+
}
25+
""",
26+
"""
27+
import io.github.pixee.security.Filenames;
28+
import org.springframework.web.multipart.MultipartFile;
29+
public class Test {
30+
public void test(MultipartFile file) {
31+
String filename = Filenames.toSimpleFilename(file.getOriginalFilename());
32+
}
33+
}
34+
""",
35+
4),
36+
new FixableSample(
37+
"""
38+
import org.springframework.web.multipart.MultipartFile;
39+
public class Test {
40+
public void test(MultipartFile file) {
41+
String filename = new File(dir, file.getOriginalFilename());
42+
}
43+
}
44+
""",
45+
"""
46+
import io.github.pixee.security.Filenames;
47+
import org.springframework.web.multipart.MultipartFile;
48+
public class Test {
49+
public void test(MultipartFile file) {
50+
String filename = new File(dir, Filenames.toSimpleFilename(file.getOriginalFilename()));
51+
}
52+
}
53+
""",
54+
4));
55+
}
56+
57+
@Override
58+
public Stream<UnfixableSample> createUnfixableSamples() {
59+
return Stream.of(
60+
// no getOriginalFilename() call
61+
new UnfixableSample(
62+
"""
63+
import org.springframework.web.multipart.MultipartFile;
64+
public class Test {
65+
public void test(MultipartFile file) {
66+
String filename = file.filename();
67+
}
68+
}
69+
""",
70+
4));
71+
}
72+
}

0 commit comments

Comments
 (0)