Skip to content

Commit 01ac8a1

Browse files
committed
feat: add user-defined allow-predicate to file path traversal sanitizer
1 parent b266ec3 commit 01ac8a1

File tree

2 files changed

+78
-22
lines changed

2 files changed

+78
-22
lines changed

sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.Optional;
2828
import java.util.concurrent.ThreadLocalRandom;
2929
import java.util.concurrent.atomic.AtomicReference;
30+
import java.util.function.Predicate;
3031
import java.util.function.Supplier;
3132

3233
/**
@@ -36,17 +37,19 @@
3637
*
3738
* <p>The default target is "../jazzer-traversal"."
3839
*
39-
* <p>Users may customize a customize the target by the BugDetectors API, e.g. by {@code
40+
* <p>Users may customize the target using the BugDetectors API, e.g. by {@code
4041
* BugDetectors.setFilePathTraversalTarget(() -> Path.of("..", "jazzer-traversal"))}.
4142
*
42-
* <p>This does not currently check for reading metadata from the target file.
43+
* <p>TODO: This sanitizer does not currently check for reading metadata from the target file.
4344
*/
4445
public class FilePathTraversal {
4546
public static final Path DEFAULT_TARGET = Paths.get("..", "jazzer-traversal");
4647

4748
// Set via reflection by Jazzer's BugDetectors API.
4849
public static final AtomicReference<Supplier<Path>> target =
4950
new AtomicReference<>(() -> DEFAULT_TARGET);
51+
public static final AtomicReference<Predicate<Path>> checkPath =
52+
new AtomicReference<>((Path ignored) -> true);
5053

5154
// When guiding the fuzzer towards the target path, sometimes both the absolute and relative paths
5255
// are valid. In this case, we toggle between them randomly.
@@ -271,20 +274,8 @@ private static void detectAndGuidePathTraversal(Object obj, int hookId) {
271274
if (obj == null) {
272275
return;
273276
}
274-
Path targetPath = target.get().get();
275277

276-
// Users can set the atomic function to return null to disable the sanitizer.
277-
if (targetPath == null) {
278-
return;
279-
}
280-
targetPath = targetPath.normalize();
281-
282-
Path currentDir = Paths.get("").toAbsolutePath();
283-
Path absTarget = toAbsolutePath(targetPath, currentDir).orElse(null);
284-
Path relTarget = toRelativePath(targetPath, currentDir).orElse(null);
285-
if (absTarget == null && relTarget == null) {
286-
return;
287-
}
278+
Path targetPath = target.get().get();
288279

289280
String query;
290281
if (obj instanceof Path) {
@@ -305,23 +296,54 @@ private static void detectAndGuidePathTraversal(Object obj, int hookId) {
305296
return;
306297
}
307298

299+
Predicate<Path> checkAllowed = checkPath.get();
300+
boolean isPathAllowed = checkAllowed == null || checkAllowed.test(Paths.get(query).normalize());
301+
if (!isPathAllowed) {
302+
Jazzer.reportFindingFromHook(
303+
new FuzzerSecurityIssueCritical(
304+
"File path traversal: "
305+
+ query
306+
+ "\n Path is not allowed by the user-defined predicate."
307+
+ "\n Current path traversal fuzzing target: "
308+
+ targetPath));
309+
}
310+
311+
// Users can set the atomic function to return null to disable the fuzzer guidance.
312+
if (targetPath == null) {
313+
return;
314+
}
315+
targetPath = targetPath.normalize();
316+
317+
Path currentDir = Paths.get("").toAbsolutePath();
318+
Path absTarget = toAbsolutePath(targetPath, currentDir).orElse(null);
319+
Path relTarget = toRelativePath(targetPath, currentDir).orElse(null);
320+
if (absTarget == null && relTarget == null) {
321+
return;
322+
}
323+
308324
if ((absTarget != null && absTarget.toString().equals(query))
309325
|| (relTarget != null && relTarget.toString().equals(query))) {
310326
Jazzer.reportFindingFromHook(
311-
new FuzzerSecurityIssueCritical("File path traversal: " + query));
327+
new FuzzerSecurityIssueCritical(
328+
"File path traversal: "
329+
+ query
330+
+ "\n Reached current path traversal fuzzing target: "
331+
+ targetPath));
312332
}
333+
313334
if (absTarget != null && relTarget != null) {
314335
if (guideTowardsAbsoluteTargetPath) {
315-
Jazzer.guideTowardsContainment(query, relTarget.toString(), hookId);
316-
} else {
317336
Jazzer.guideTowardsContainment(query, absTarget.toString(), hookId);
337+
} else {
338+
Jazzer.guideTowardsContainment(query, relTarget.toString(), hookId);
318339
}
319340
if (--toggleCounter <= 0) {
320341
guideTowardsAbsoluteTargetPath = !guideTowardsAbsoluteTargetPath;
321342
toggleCounter = ThreadLocalRandom.current().nextInt(1, MAX_TARGET_FOCUS_COUNT + 1);
322343
}
323-
} else
344+
} else {
324345
Jazzer.guideTowardsContainment(
325346
query, (absTarget != null ? absTarget : relTarget).toString(), hookId);
347+
}
326348
}
327349
}

src/main/java/com/code_intelligence/jazzer/api/BugDetectors.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.nio.file.Path;
2020
import java.util.concurrent.atomic.AtomicReference;
2121
import java.util.function.BiPredicate;
22+
import java.util.function.Predicate;
2223
import java.util.function.Supplier;
2324

2425
/** Provides static functions that configure the behavior of bug detectors provided by Jazzer. */
@@ -88,11 +89,14 @@ public static SilentCloseable allowNetworkConnections(
8889
getSanitizerVariable("com.code_intelligence.jazzer.sanitizers.FilePathTraversal", "target");
8990

9091
/**
91-
* Sets the target for file path traversal sanitization.
92+
* Sets the target for file path traversal sanitization. If the target is reached, a finding is
93+
* thrown. The target is also used to guide the fuzzer to intentionally trigger file path
94+
* traversal.
9295
*
93-
* <p>By default, the file path traversal target is set to {@code "../jazzer-traversal"}.
96+
* <p>By default, the file path traversal target is set to return {@code "../jazzer-traversal"}.
9497
*
95-
* <p>Setting the target to {@code () -> null } will disable file path traversal sanitization.
98+
* <p>Setting the path traversal target supplier to return {@code null } will disable the
99+
* guidance.
96100
*
97101
* <p>By wrapping the call into a try-with-resources statement, the target can be configured to
98102
* apply to individual parts of the fuzz test only:
@@ -111,6 +115,36 @@ public static SilentCloseable setFilePathTraversalTarget(Supplier<Path> pathTrav
111115
return setSanitizerVariable(pathTraversalTarget, currentPathTraversalTarget);
112116
}
113117

118+
private static final AtomicReference<Predicate<Path>> currentCheckPath =
119+
getSanitizerVariable(
120+
"com.code_intelligence.jazzer.sanitizers.FilePathTraversal", "checkPath");
121+
122+
/**
123+
* Sets the predicate that determines if a file path is allowed to be accessed. Paths that are not
124+
* allowed will trigger a file path traversal finding. If you use this method, don't forget to set
125+
* the fuzzing target with {@code setFilePathTraversalTarget} that aligns with this predicate,
126+
* because both {@code target} and {@code checkPath} can trigger a finding independently.
127+
*
128+
* <p>By default, all file paths are allowed. Setting the predicate to {@code false} will trigger
129+
* a file path traversal finding for any file path access.
130+
*
131+
* <p>By wrapping the call into a try-with-resources statement, the predicate can be configured to
132+
* apply to individual parts of the fuzz test only:
133+
*
134+
* <pre>{@code
135+
* try (SilentCloseable unused = BugDetectors.setFilePathTraversalAllowPath(
136+
* (Path p) -> p.toString().contains("secret"))) {
137+
* // Perform operations that require file path traversal sanitization
138+
* }
139+
* }</pre>
140+
*
141+
* @param checkPath a predicate that evaluates to {@code true} if the file path is allowed
142+
* @return a {@link SilentCloseable} that restores the previously set predicate when closed
143+
*/
144+
public static SilentCloseable setFilePathTraversalAllowPath(Predicate<Path> checkPath) {
145+
return setSanitizerVariable(checkPath, currentCheckPath);
146+
}
147+
114148
private static <T> AtomicReference<T> getSanitizerVariable(
115149
String sanitizerClassName, String fieldName) {
116150
try {

0 commit comments

Comments
 (0)