Skip to content

Commit 6623706

Browse files
committed
feat: use BugDetectors API to control the path traversal sanitizer
1 parent 8eeef83 commit 6623706

File tree

6 files changed

+311
-122
lines changed

6 files changed

+311
-122
lines changed

sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ java_library(
1212
java_library(
1313
name = "file_path_traversal",
1414
srcs = ["FilePathTraversal.java"],
15+
visibility = [
16+
"//sanitizers:__pkg__",
17+
"//sanitizers/src/test/java/com/code_intelligence/jazzer/sanitizers:__pkg__",
18+
],
1519
deps = ["//src/main/java/com/code_intelligence/jazzer/api:hooks"],
1620
)
1721

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

Lines changed: 92 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -24,59 +24,63 @@
2424
import java.nio.file.InvalidPathException;
2525
import java.nio.file.Path;
2626
import java.nio.file.Paths;
27-
import java.util.logging.Level;
28-
import java.util.logging.Logger;
27+
import java.util.Optional;
28+
import java.util.concurrent.ThreadLocalRandom;
29+
import java.util.concurrent.atomic.AtomicReference;
30+
import java.util.function.Supplier;
2931

3032
/**
3133
* This tests for a file read or write of a specific file path whether relative or absolute.
3234
*
3335
* <p>This checks only for literal, absolute, normalized paths. It does not process symbolic links.
3436
*
35-
* <p>The default target is {@link FilePathTraversal#DEFAULT_TARGET_STRING}
37+
* <p>The default target is "../jazzer-traversal"."
3638
*
37-
* <p>Users may customize a customize the target by setting the full path in the environment
38-
* variable {@link FilePathTraversal#FILE_PATH_TARGET_KEY}
39+
* <p>Users may customize a customize the target by the BugDetectors API, e.g. by {@code
40+
* BugDetectors.setFilePathTraversalTarget(() -> Path.of("..", "jazzer-traversal"))}.
3941
*
4042
* <p>This does not currently check for reading metadata from the target file.
4143
*/
4244
public class FilePathTraversal {
43-
public static final String FILE_PATH_TARGET_KEY = "jazzer.file_path_traversal_target";
44-
public static final String DEFAULT_TARGET_STRING = "../jazzer-traversal";
45+
public static final Path DEFAULT_TARGET = Paths.get("..", "jazzer-traversal");
4546

46-
private static final Logger LOG = Logger.getLogger(FilePathTraversal.class.getName());
47+
// Set via reflection by Jazzer's BugDetectors API.
48+
public static final AtomicReference<Supplier<Path>> target =
49+
new AtomicReference<>(() -> DEFAULT_TARGET);
4750

48-
private static Path RELATIVE_TARGET;
49-
private static Path ABSOLUTE_TARGET;
50-
private static boolean IS_DISABLED = false;
51-
private static boolean IS_SET_UP = false;
51+
// When guiding the fuzzer towards the target path, sometimes both the absolute and relative paths
52+
// are valid. In this case, we toggle between them randomly.
53+
// The random part is important because it is possible to set several targets in a fuzz test with
54+
// try(target1...){
55+
// ...
56+
// try(target2...) {
57+
// ...
58+
// If we toggle in fix pattern, the fuzzer might guide towards the same blocks towards the same
59+
// target.
60+
// Randomizing the toggle counter sidesteps this issue.
61+
private static final int MAX_TARGET_FOCUS_COUNT = 23;
62+
private static boolean guideTowardsAbsoluteTargetPath = true;
63+
private static int toggleCounter = 1;
5264

53-
private static void setUp() {
54-
String customTarget = System.getProperty(FILE_PATH_TARGET_KEY);
55-
if (customTarget != null && !customTarget.isEmpty()) {
56-
LOG.log(Level.FINE, "custom target loaded: " + customTarget);
57-
setTargets(customTarget);
58-
} else {
59-
// check that this isn't being run at the root directory
60-
Path cwd = Paths.get(".").toAbsolutePath();
61-
if (cwd.getParent() == null) {
62-
LOG.warning(
63-
"Can't run from the root directory with the default target. "
64-
+ "The FilePathTraversal sanitizer is disabled.");
65-
IS_DISABLED = true;
65+
public static Optional<Path> toAbsolutePath(Path path, Path currentDir) {
66+
try {
67+
if (path.isAbsolute()) {
68+
return Optional.of(path.normalize());
6669
}
67-
setTargets(DEFAULT_TARGET_STRING);
70+
return Optional.of(currentDir.resolve(path).normalize());
71+
} catch (InvalidPathException e) {
72+
return Optional.empty();
6873
}
6974
}
7075

71-
private static void setTargets(String targetPath) {
72-
Path p = Paths.get(targetPath);
73-
Path pwd = Paths.get(".");
74-
if (p.isAbsolute()) {
75-
ABSOLUTE_TARGET = p.toAbsolutePath().normalize();
76-
RELATIVE_TARGET = pwd.toAbsolutePath().relativize(ABSOLUTE_TARGET).normalize();
77-
} else {
78-
ABSOLUTE_TARGET = pwd.resolve(p).toAbsolutePath().normalize();
79-
RELATIVE_TARGET = p.normalize();
76+
public static Optional<Path> toRelativePath(Path path, Path currentDir) {
77+
try {
78+
if (path.isAbsolute()) {
79+
return Optional.of(currentDir.relativize(path).normalize());
80+
}
81+
return Optional.of(path.normalize());
82+
} catch (IllegalArgumentException e) {
83+
return Optional.empty();
8084
}
8185
}
8286

@@ -172,21 +176,11 @@ private static void setTargets(String targetPath) {
172176
public static void pathFirstArgHook(
173177
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
174178
if (arguments.length > 0) {
175-
Object argObj = arguments[0];
176-
if (argObj instanceof Path) {
177-
checkPath((Path) argObj, hookId);
178-
}
179+
detectAndGuidePathTraversal(arguments[0], hookId);
179180
}
180181
}
181182

182-
/**
183-
* Checks to confirm that a path that is read from or written to is in an allowed directory.
184-
*
185-
* @param method
186-
* @param thisObject
187-
* @param arguments
188-
* @param hookId
189-
*/
183+
/** Checks to confirm that a path that is read from or written to is in an allowed directory. */
190184
@MethodHook(
191185
type = HookType.BEFORE,
192186
targetClassName = "java.nio.file.Files",
@@ -202,14 +196,8 @@ public static void pathFirstArgHook(
202196
public static void copyMismatchMvHook(
203197
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
204198
if (arguments.length > 1) {
205-
Object from = arguments[0];
206-
if (from instanceof Path) {
207-
checkPath((Path) from, hookId);
208-
}
209-
Object to = arguments[1];
210-
if (to instanceof Path) {
211-
checkPath((Path) to, hookId);
212-
}
199+
detectAndGuidePathTraversal(arguments[0], hookId);
200+
detectAndGuidePathTraversal(arguments[1], hookId);
213201
}
214202
}
215203

@@ -220,7 +208,7 @@ public static void copyMismatchMvHook(
220208
public static void fileReaderHook(
221209
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
222210
if (arguments.length > 0) {
223-
checkObj(arguments[0], hookId);
211+
detectAndGuidePathTraversal(arguments[0], hookId);
224212
}
225213
}
226214

@@ -231,7 +219,7 @@ public static void fileReaderHook(
231219
public static void fileWriterHook(
232220
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
233221
if (arguments.length > 0) {
234-
checkObj(arguments[0], hookId);
222+
detectAndGuidePathTraversal(arguments[0], hookId);
235223
}
236224
}
237225

@@ -242,7 +230,7 @@ public static void fileWriterHook(
242230
public static void fileInputStreamHook(
243231
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
244232
if (arguments.length > 0) {
245-
checkObj(arguments[0], hookId);
233+
detectAndGuidePathTraversal(arguments[0], hookId);
246234
}
247235
}
248236

@@ -253,7 +241,7 @@ public static void fileInputStreamHook(
253241
public static void processFileOutputStartHook(
254242
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
255243
if (arguments.length > 0) {
256-
checkObj(arguments[0], hookId);
244+
detectAndGuidePathTraversal(arguments[0], hookId);
257245
}
258246
}
259247

@@ -264,7 +252,7 @@ public static void processFileOutputStartHook(
264252
public static void scannerHook(
265253
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
266254
if (arguments.length > 0) {
267-
checkObj(arguments[0], hookId);
255+
detectAndGuidePathTraversal(arguments[0], hookId);
268256
}
269257
}
270258

@@ -275,82 +263,65 @@ public static void scannerHook(
275263
public static void fileOutputStreamHook(
276264
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
277265
if (arguments.length > 0) {
278-
checkObj(arguments[0], hookId);
266+
detectAndGuidePathTraversal(arguments[0], hookId);
279267
}
280268
}
281269

282-
private static void checkObj(Object obj, int hookId) {
283-
if (obj instanceof String) {
284-
checkString((String) obj, hookId);
285-
} else if (obj instanceof Path) {
286-
checkPath((Path) obj, hookId);
287-
} else if (obj instanceof File) {
288-
checkFile((File) obj, hookId);
270+
private static void detectAndGuidePathTraversal(Object obj, int hookId) {
271+
if (obj == null) {
272+
return;
289273
}
290-
}
274+
Path targetPath = target.get().get();
291275

292-
private static void checkPath(Path p, int hookId) {
293-
check(p);
294-
Path normalized = p.normalize();
295-
if (p.isAbsolute()) {
296-
Jazzer.guideTowardsEquality(normalized.toString(), ABSOLUTE_TARGET.toString(), hookId);
297-
} else {
298-
Jazzer.guideTowardsEquality(normalized.toString(), RELATIVE_TARGET.toString(), hookId);
299-
}
300-
}
301-
302-
private static void checkFile(File f, int hookId) {
303-
try {
304-
check(f.toPath());
305-
} catch (InvalidPathException e) {
306-
// TODO: give up -- for now
276+
// Users can set the atomic function to return null to disable the sanitizer.
277+
if (targetPath == null) {
307278
return;
308279
}
309-
Path normalized = f.toPath().normalize();
310-
if (normalized.isAbsolute()) {
311-
Jazzer.guideTowardsEquality(normalized.toString(), ABSOLUTE_TARGET.toString(), hookId);
312-
} else {
313-
Jazzer.guideTowardsEquality(normalized.toString(), RELATIVE_TARGET.toString(), hookId);
314-
}
315-
}
280+
targetPath = targetPath.normalize();
316281

317-
private static void checkString(String s, int hookId) {
318-
try {
319-
check(Paths.get(s));
320-
} catch (InvalidPathException e) {
321-
checkFile(new File(s), hookId);
322-
// TODO -- give up for now
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) {
323286
return;
324287
}
325-
Path normalized = Paths.get(s);
326-
if (normalized.isAbsolute()) {
327-
Jazzer.guideTowardsEquality(s, ABSOLUTE_TARGET.toString(), hookId);
328-
} else {
329-
Jazzer.guideTowardsEquality(s, RELATIVE_TARGET.toString(), hookId);
330-
}
331-
}
332288

333-
private static void check(Path p) {
334-
// super lazy initialization -- race condition with unit test if this is set in a static block
335-
synchronized (LOG) {
336-
if (!IS_SET_UP) {
337-
setUp();
338-
IS_SET_UP = true;
289+
String query;
290+
if (obj instanceof Path) {
291+
query = ((Path) obj).normalize().toString();
292+
} else if (obj instanceof File) {
293+
try {
294+
query = ((File) obj).toPath().normalize().toString();
295+
} catch (InvalidPathException e) {
296+
return;
339297
}
340-
}
341-
if (IS_DISABLED) {
298+
} else if (obj instanceof String) {
299+
try {
300+
query = (String) obj;
301+
} catch (InvalidPathException e) {
302+
return;
303+
}
304+
} else { // not a path, file or string
342305
return;
343306
}
344307

345-
// catch all exceptions that might be thrown by the sanitizer
346-
Path normalized;
347-
try {
348-
normalized = p.toAbsolutePath().normalize();
349-
} catch (Throwable e) {
350-
return;
351-
}
352-
if (normalized.equals(ABSOLUTE_TARGET)) {
353-
Jazzer.reportFindingFromHook(new FuzzerSecurityIssueCritical("File path traversal: " + p));
308+
if ((absTarget != null && absTarget.toString().equals(query))
309+
|| (relTarget != null && relTarget.toString().equals(query))) {
310+
Jazzer.reportFindingFromHook(
311+
new FuzzerSecurityIssueCritical("File path traversal: " + query));
354312
}
313+
if (absTarget != null && relTarget != null) {
314+
if (guideTowardsAbsoluteTargetPath) {
315+
Jazzer.guideTowardsContainment(query, relTarget.toString(), hookId);
316+
} else {
317+
Jazzer.guideTowardsContainment(query, absTarget.toString(), hookId);
318+
}
319+
if (--toggleCounter <= 0) {
320+
guideTowardsAbsoluteTargetPath = !guideTowardsAbsoluteTargetPath;
321+
toggleCounter = ThreadLocalRandom.current().nextInt(1, MAX_TARGET_FOCUS_COUNT + 1);
322+
}
323+
} else
324+
Jazzer.guideTowardsContainment(
325+
query, (absTarget != null ? absTarget : relTarget).toString(), hookId);
355326
}
356327
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
load("@contrib_rules_jvm//java:defs.bzl", "JUNIT5_DEPS", "java_junit5_test")
2+
3+
java_junit5_test(
4+
name = "FilePathTraversalTest",
5+
srcs = ["FilePathTraversalTest.java"],
6+
deps = JUNIT5_DEPS + [
7+
"//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers:file_path_traversal",
8+
"@maven//:com_google_truth_truth",
9+
"@maven//:org_junit_jupiter_junit_jupiter_api",
10+
"@maven//:org_junit_jupiter_junit_jupiter_params",
11+
],
12+
)

0 commit comments

Comments
 (0)