Skip to content

Commit efbc635

Browse files
tballisonoetr
andcommitted
add a path traversal sanitizer
Co-authored-by: Peter Samarin <petersamarin@gmail.com>
1 parent fed0080 commit efbc635

File tree

6 files changed

+489
-0
lines changed

6 files changed

+489
-0
lines changed

sanitizers/sanitizers.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ _sanitizer_class_names = [
2121
"ClojureLangHooks",
2222
"Deserialization",
2323
"ExpressionLanguageInjection",
24+
"FilePathTraversal",
2425
"LdapInjection",
2526
"NamingContextLookup",
2627
"OsCommandInjection",

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ java_library(
99
deps = ["//src/main/java/com/code_intelligence/jazzer/api:hooks"],
1010
)
1111

12+
java_library(
13+
name = "file_path_traversal",
14+
srcs = ["FilePathTraversal.java"],
15+
deps = ["//src/main/java/com/code_intelligence/jazzer/api:hooks"],
16+
)
17+
1218
java_library(
1319
name = "regex_roadblocks",
1420
srcs = ["RegexRoadblocks.java"],
@@ -58,6 +64,7 @@ kt_jvm_library(
5864
visibility = ["//sanitizers:__pkg__"],
5965
runtime_deps = [
6066
":clojure_lang_hooks",
67+
":file_path_traversal",
6168
":regex_roadblocks",
6269
":script_engine_injection",
6370
":server_side_request_forgery",
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
/*
2+
* Copyright 2025 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.code_intelligence.jazzer.sanitizers;
17+
18+
import com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical;
19+
import com.code_intelligence.jazzer.api.HookType;
20+
import com.code_intelligence.jazzer.api.Jazzer;
21+
import com.code_intelligence.jazzer.api.MethodHook;
22+
import java.io.File;
23+
import java.lang.invoke.MethodHandle;
24+
import java.nio.file.InvalidPathException;
25+
import java.nio.file.Path;
26+
import java.nio.file.Paths;
27+
import java.util.logging.Level;
28+
import java.util.logging.Logger;
29+
30+
/**
31+
* This tests for a file read or write of a specific file path whether relative or absolute.
32+
*
33+
* <p>This checks only for literal, absolute, normalized paths. It does not process symbolic links.
34+
*
35+
* <p>The default target is {@link FilePathTraversal#DEFAULT_TARGET_STRING}
36+
*
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+
*
40+
* <p>This does not currently check for reading metadata from the target file.
41+
*/
42+
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+
46+
private static final Logger LOG = Logger.getLogger(FilePathTraversal.class.getName());
47+
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;
52+
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;
66+
}
67+
setTargets(DEFAULT_TARGET_STRING);
68+
}
69+
}
70+
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();
80+
}
81+
}
82+
83+
// intentionally skipping createLink and createSymbolicLink
84+
@MethodHook(
85+
type = HookType.BEFORE,
86+
targetClassName = "java.nio.file.Files",
87+
targetMethod = "createDirectory")
88+
@MethodHook(
89+
type = HookType.BEFORE,
90+
targetClassName = "java.nio.file.Files",
91+
targetMethod = "createDirectories")
92+
@MethodHook(
93+
type = HookType.BEFORE,
94+
targetClassName = "java.nio.file.Files",
95+
targetMethod = "createFile")
96+
@MethodHook(
97+
type = HookType.BEFORE,
98+
targetClassName = "java.nio.file.Files",
99+
targetMethod = "createTempDirectory")
100+
@MethodHook(
101+
type = HookType.BEFORE,
102+
targetClassName = "java.nio.file.Files",
103+
targetMethod = "createTempFile")
104+
@MethodHook(
105+
type = HookType.BEFORE,
106+
targetClassName = "java.nio.file.Files",
107+
targetMethod = "delete")
108+
@MethodHook(
109+
type = HookType.BEFORE,
110+
targetClassName = "java.nio.file.Files",
111+
targetMethod = "deleteIfExists")
112+
@MethodHook(
113+
type = HookType.BEFORE,
114+
targetClassName = "java.nio.file.Files",
115+
targetMethod = "lines")
116+
@MethodHook(
117+
type = HookType.BEFORE,
118+
targetClassName = "java.nio.file.Files",
119+
targetMethod = "newByteChannel")
120+
@MethodHook(
121+
type = HookType.BEFORE,
122+
targetClassName = "java.nio.file.Files",
123+
targetMethod = "newBufferedReader")
124+
@MethodHook(
125+
type = HookType.BEFORE,
126+
targetClassName = "java.nio.file.Files",
127+
targetMethod = "newBufferedWriter")
128+
@MethodHook(
129+
type = HookType.BEFORE,
130+
targetClassName = "java.nio.file.Files",
131+
targetMethod = "readString")
132+
@MethodHook(
133+
type = HookType.BEFORE,
134+
targetClassName = "java.nio.file.Files",
135+
targetMethod = "newBufferedReader")
136+
@MethodHook(
137+
type = HookType.BEFORE,
138+
targetClassName = "java.nio.file.Files",
139+
targetMethod = "readAllBytes")
140+
@MethodHook(
141+
type = HookType.BEFORE,
142+
targetClassName = "java.nio.file.Files",
143+
targetMethod = "readAllLines")
144+
@MethodHook(
145+
type = HookType.BEFORE,
146+
targetClassName = "java.nio.file.Files",
147+
targetMethod = "readSymbolicLink")
148+
@MethodHook(
149+
type = HookType.BEFORE,
150+
targetClassName = "java.nio.file.Files",
151+
targetMethod = "write")
152+
@MethodHook(
153+
type = HookType.BEFORE,
154+
targetClassName = "java.nio.file.Files",
155+
targetMethod = "writeString")
156+
@MethodHook(
157+
type = HookType.BEFORE,
158+
targetClassName = "java.nio.file.Files",
159+
targetMethod = "newInputStream")
160+
@MethodHook(
161+
type = HookType.BEFORE,
162+
targetClassName = "java.nio.file.Files",
163+
targetMethod = "newOutputStream")
164+
@MethodHook(
165+
type = HookType.BEFORE,
166+
targetClassName = "java.nio.file.probeContentType",
167+
targetMethod = "open")
168+
@MethodHook(
169+
type = HookType.BEFORE,
170+
targetClassName = "java.nio.channels.FileChannel",
171+
targetMethod = "open")
172+
public static void pathFirstArgHook(
173+
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
174+
if (arguments.length > 0) {
175+
Object argObj = arguments[0];
176+
if (argObj instanceof Path) {
177+
checkPath((Path) argObj, hookId);
178+
}
179+
}
180+
}
181+
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+
*/
190+
@MethodHook(
191+
type = HookType.BEFORE,
192+
targetClassName = "java.nio.file.Files",
193+
targetMethod = "copy")
194+
@MethodHook(
195+
type = HookType.BEFORE,
196+
targetClassName = "java.nio.file.Files",
197+
targetMethod = "mismatch")
198+
@MethodHook(
199+
type = HookType.BEFORE,
200+
targetClassName = "java.nio.file.Files",
201+
targetMethod = "move")
202+
public static void copyMismatchMvHook(
203+
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
204+
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+
}
213+
}
214+
}
215+
216+
@MethodHook(
217+
type = HookType.BEFORE,
218+
targetClassName = "java.io.FileReader",
219+
targetMethod = "<init>")
220+
public static void fileReaderHook(
221+
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
222+
if (arguments.length > 0) {
223+
checkObj(arguments[0], hookId);
224+
}
225+
}
226+
227+
@MethodHook(
228+
type = HookType.BEFORE,
229+
targetClassName = "java.io.FileWriter",
230+
targetMethod = "<init>")
231+
public static void fileWriterHook(
232+
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
233+
if (arguments.length > 0) {
234+
checkObj(arguments[0], hookId);
235+
}
236+
}
237+
238+
@MethodHook(
239+
type = HookType.BEFORE,
240+
targetClassName = "java.io.FileInputStream",
241+
targetMethod = "<init>")
242+
public static void fileInputStreamHook(
243+
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
244+
if (arguments.length > 0) {
245+
checkObj(arguments[0], hookId);
246+
}
247+
}
248+
249+
@MethodHook(
250+
type = HookType.BEFORE,
251+
targetClassName = "java.io.FileOutputStream",
252+
targetMethod = "<init>")
253+
public static void processFileOutputStartHook(
254+
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
255+
if (arguments.length > 0) {
256+
checkObj(arguments[0], hookId);
257+
}
258+
}
259+
260+
@MethodHook(
261+
type = HookType.BEFORE,
262+
targetClassName = "java.util.Scanner",
263+
targetMethod = "<init>")
264+
public static void scannerHook(
265+
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
266+
if (arguments.length > 0) {
267+
checkObj(arguments[0], hookId);
268+
}
269+
}
270+
271+
@MethodHook(
272+
type = HookType.BEFORE,
273+
targetClassName = "java.io.FileOutputStream",
274+
targetMethod = "<init>")
275+
public static void fileOutputStreamHook(
276+
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
277+
if (arguments.length > 0) {
278+
checkObj(arguments[0], hookId);
279+
}
280+
}
281+
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);
289+
}
290+
}
291+
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
307+
return;
308+
}
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+
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
323+
return;
324+
}
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+
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;
339+
}
340+
}
341+
if (IS_DISABLED) {
342+
return;
343+
}
344+
345+
if (p.toAbsolutePath().normalize().equals(ABSOLUTE_TARGET)) {
346+
Jazzer.reportFindingFromHook(new FuzzerSecurityIssueCritical("File path traversal: " + p));
347+
}
348+
}
349+
}

0 commit comments

Comments
 (0)