2424import java .nio .file .InvalidPathException ;
2525import java .nio .file .Path ;
2626import 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 */
4244public 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}
0 commit comments