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 , 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}
0 commit comments