24
24
import java .nio .file .InvalidPathException ;
25
25
import java .nio .file .Path ;
26
26
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 ;
29
31
30
32
/**
31
33
* This tests for a file read or write of a specific file path whether relative or absolute.
32
34
*
33
35
* <p>This checks only for literal, absolute, normalized paths. It does not process symbolic links.
34
36
*
35
- * <p>The default target is {@link FilePathTraversal#DEFAULT_TARGET_STRING}
37
+ * <p>The default target is "../jazzer-traversal"."
36
38
*
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"))}.
39
41
*
40
42
* <p>This does not currently check for reading metadata from the target file.
41
43
*/
42
44
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" );
45
46
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 );
47
50
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 ;
52
64
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 ());
66
69
}
67
- setTargets (DEFAULT_TARGET_STRING );
70
+ return Optional .of (path .toAbsolutePath ().normalize ());
71
+ } catch (InvalidPathException e ) {
72
+ return Optional .empty ();
68
73
}
69
74
}
70
75
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 ();
80
86
}
81
87
}
82
88
@@ -172,21 +178,11 @@ private static void setTargets(String targetPath) {
172
178
public static void pathFirstArgHook (
173
179
MethodHandle method , Object thisObject , Object [] arguments , int hookId ) {
174
180
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 );
179
182
}
180
183
}
181
184
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. */
190
186
@ MethodHook (
191
187
type = HookType .BEFORE ,
192
188
targetClassName = "java.nio.file.Files" ,
@@ -202,14 +198,8 @@ public static void pathFirstArgHook(
202
198
public static void copyMismatchMvHook (
203
199
MethodHandle method , Object thisObject , Object [] arguments , int hookId ) {
204
200
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 );
213
203
}
214
204
}
215
205
@@ -220,7 +210,7 @@ public static void copyMismatchMvHook(
220
210
public static void fileReaderHook (
221
211
MethodHandle method , Object thisObject , Object [] arguments , int hookId ) {
222
212
if (arguments .length > 0 ) {
223
- checkObj (arguments [0 ], hookId );
213
+ detectAndGuidePathTraversal (arguments [0 ], hookId );
224
214
}
225
215
}
226
216
@@ -231,7 +221,7 @@ public static void fileReaderHook(
231
221
public static void fileWriterHook (
232
222
MethodHandle method , Object thisObject , Object [] arguments , int hookId ) {
233
223
if (arguments .length > 0 ) {
234
- checkObj (arguments [0 ], hookId );
224
+ detectAndGuidePathTraversal (arguments [0 ], hookId );
235
225
}
236
226
}
237
227
@@ -242,7 +232,7 @@ public static void fileWriterHook(
242
232
public static void fileInputStreamHook (
243
233
MethodHandle method , Object thisObject , Object [] arguments , int hookId ) {
244
234
if (arguments .length > 0 ) {
245
- checkObj (arguments [0 ], hookId );
235
+ detectAndGuidePathTraversal (arguments [0 ], hookId );
246
236
}
247
237
}
248
238
@@ -253,7 +243,7 @@ public static void fileInputStreamHook(
253
243
public static void processFileOutputStartHook (
254
244
MethodHandle method , Object thisObject , Object [] arguments , int hookId ) {
255
245
if (arguments .length > 0 ) {
256
- checkObj (arguments [0 ], hookId );
246
+ detectAndGuidePathTraversal (arguments [0 ], hookId );
257
247
}
258
248
}
259
249
@@ -264,7 +254,7 @@ public static void processFileOutputStartHook(
264
254
public static void scannerHook (
265
255
MethodHandle method , Object thisObject , Object [] arguments , int hookId ) {
266
256
if (arguments .length > 0 ) {
267
- checkObj (arguments [0 ], hookId );
257
+ detectAndGuidePathTraversal (arguments [0 ], hookId );
268
258
}
269
259
}
270
260
@@ -275,82 +265,63 @@ public static void scannerHook(
275
265
public static void fileOutputStreamHook (
276
266
MethodHandle method , Object thisObject , Object [] arguments , int hookId ) {
277
267
if (arguments .length > 0 ) {
278
- checkObj (arguments [0 ], hookId );
268
+ detectAndGuidePathTraversal (arguments [0 ], hookId );
279
269
}
280
270
}
281
271
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 ;
289
275
}
290
- }
276
+ Path targetPath = target . get (). get ();
291
277
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 ) {
307
280
return ;
308
281
}
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
- }
316
282
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 ) {
323
286
return ;
324
287
}
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
- }
332
288
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 ;
339
297
}
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
342
305
return ;
343
306
}
344
307
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 ));
354
312
}
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 );
355
326
}
356
327
}
0 commit comments