Skip to content

Commit 235057d

Browse files
committed
feat: use BugDetectors API to control the path traversal sanitizer
1 parent 44a45d8 commit 235057d

File tree

3 files changed

+157
-122
lines changed

3 files changed

+157
-122
lines changed

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,65 @@
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) {
66+
try {
67+
if (path.isAbsolute()) {
68+
return Optional.of(path.normalize());
6669
}
67-
setTargets(DEFAULT_TARGET_STRING);
70+
return Optional.of(path.toAbsolutePath().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) {
77+
try {
78+
if (path.isAbsolute()) {
79+
// If the path is absolute, we return the relative path to the current working directory.
80+
Path currentDir = Paths.get(".").toAbsolutePath();
81+
return Optional.of(path.relativize(currentDir).normalize());
82+
}
83+
return Optional.of(path.normalize());
84+
} catch (IllegalArgumentException e) {
85+
return Optional.empty();
8086
}
8187
}
8288

@@ -172,21 +178,11 @@ private static void setTargets(String targetPath) {
172178
public static void pathFirstArgHook(
173179
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
174180
if (arguments.length > 0) {
175-
Object argObj = arguments[0];
176-
if (argObj instanceof Path) {
177-
checkPath((Path) argObj, hookId);
178-
}
181+
detectAndGuidePathTraversal(arguments[0], hookId);
179182
}
180183
}
181184

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-
*/
185+
/** Checks to confirm that a path that is read from or written to is in an allowed directory. */
190186
@MethodHook(
191187
type = HookType.BEFORE,
192188
targetClassName = "java.nio.file.Files",
@@ -202,14 +198,8 @@ public static void pathFirstArgHook(
202198
public static void copyMismatchMvHook(
203199
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
204200
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-
}
201+
detectAndGuidePathTraversal(arguments[0], hookId);
202+
detectAndGuidePathTraversal(arguments[1], hookId);
213203
}
214204
}
215205

@@ -220,7 +210,7 @@ public static void copyMismatchMvHook(
220210
public static void fileReaderHook(
221211
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
222212
if (arguments.length > 0) {
223-
checkObj(arguments[0], hookId);
213+
detectAndGuidePathTraversal(arguments[0], hookId);
224214
}
225215
}
226216

@@ -231,7 +221,7 @@ public static void fileReaderHook(
231221
public static void fileWriterHook(
232222
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
233223
if (arguments.length > 0) {
234-
checkObj(arguments[0], hookId);
224+
detectAndGuidePathTraversal(arguments[0], hookId);
235225
}
236226
}
237227

@@ -242,7 +232,7 @@ public static void fileWriterHook(
242232
public static void fileInputStreamHook(
243233
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
244234
if (arguments.length > 0) {
245-
checkObj(arguments[0], hookId);
235+
detectAndGuidePathTraversal(arguments[0], hookId);
246236
}
247237
}
248238

@@ -253,7 +243,7 @@ public static void fileInputStreamHook(
253243
public static void processFileOutputStartHook(
254244
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
255245
if (arguments.length > 0) {
256-
checkObj(arguments[0], hookId);
246+
detectAndGuidePathTraversal(arguments[0], hookId);
257247
}
258248
}
259249

@@ -264,7 +254,7 @@ public static void processFileOutputStartHook(
264254
public static void scannerHook(
265255
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
266256
if (arguments.length > 0) {
267-
checkObj(arguments[0], hookId);
257+
detectAndGuidePathTraversal(arguments[0], hookId);
268258
}
269259
}
270260

@@ -275,82 +265,63 @@ public static void scannerHook(
275265
public static void fileOutputStreamHook(
276266
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
277267
if (arguments.length > 0) {
278-
checkObj(arguments[0], hookId);
268+
detectAndGuidePathTraversal(arguments[0], hookId);
279269
}
280270
}
281271

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);
272+
private static void detectAndGuidePathTraversal(Object obj, int hookId) {
273+
if (obj == null) {
274+
return;
289275
}
290-
}
276+
Path targetPath = target.get().get();
291277

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
278+
// Users can set the atomic function to return null to disable the sanitizer.
279+
if (targetPath == null) {
307280
return;
308281
}
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-
}
316282

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
283+
Path absTarget = toAbsolutePath(targetPath).orElse(null);
284+
Path relTarget = toRelativePath(targetPath).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
}

sanitizers/src/test/java/com/example/AbsoluteFilePathTraversal.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.example;
1818

19+
import com.code_intelligence.jazzer.api.BugDetectors;
1920
import com.code_intelligence.jazzer.mutation.annotation.NotNull;
2021
import com.code_intelligence.jazzer.mutation.annotation.WithUtf8Length;
2122
import java.io.BufferedReader;
@@ -28,7 +29,8 @@
2829

2930
public class AbsoluteFilePathTraversal {
3031
static {
31-
System.setProperty("jazzer.file_path_traversal_target", "/custom/path/jazzer-traversal");
32+
BugDetectors.setFilePathTraversalTarget(
33+
() -> Paths.get("/", "custom", "path", "jazzer-traversal"));
3234
}
3335

3436
public static void fuzzerTestOneInput(@WithUtf8Length(max = 100) @NotNull String path) {

0 commit comments

Comments
 (0)