Skip to content

Commit 16f5eb8

Browse files
authored
feat(appsec): extend RASP callsite coverage to File-argument constructors of FileOutputStream and FileInputStream (#11113)
* feat(rasp): instrument FileOutputStream/FileInputStream(File) constructors Add RASP callsite coverage for File-argument constructors that were previously not instrumented: - FileOutputStream(File) and FileOutputStream(File, boolean): call FileIORaspHelper.INSTANCE.beforeFileWritten(file.getPath()) - FileInputStream(File): call FileIORaspHelper.INSTANCE.beforeFileLoaded(file.getPath()) No IAST changes — the File-based constructors delegate path resolution to the JVM, so IAST taint tracking via the String constructor already covers those code paths at a higher level. Tests added following the existing RASP test pattern. * feat(appsec): extend RASP file I/O coverage to FileReader, FileWriter, RandomAccessFile, Files.* and FileChannel Extends RASP callsite instrumentation (APPSEC-61874) beyond FileInputStream/FileOutputStream to all remaining Java file I/O APIs that were not covered. No IAST changes. New callsites: - FileReaderCallSite: FileReader(String/File) + Java 11+ Charset variants → beforeFileLoaded - FileWriterCallSite: FileWriter(String/File/boolean) + Java 11+ Charset variants → beforeFileWritten - RandomAccessFileCallSite: RandomAccessFile(String/File, mode) → beforeFileLoaded for "r", both beforeFileLoaded + beforeFileWritten for "rw"/"rws"/"rwd" - FilesCallSite: all Files.* read and write methods (newOutputStream, copy(IS,Path), write, writeString, newBufferedWriter, move, newInputStream, readAllBytes, readAllLines, readString, newBufferedReader, lines) - FileChannelCallSite: FileChannel.open(Path, ...) → fires both read and write callbacks Extended callsites: - PathCallSite: add resolve(Path) and resolveSibling(Path) → beforeFileLoaded - PathsCallSite: add Path.of(String[], URI) (Java 11+) → beforeFileLoaded FileIORaspHelper: add beforeRandomAccessFileOpened(path, mode) helper Relates to #11084 and #11113 * Apply spotless formatting * Add Java 11 test suite for RASP coverage of Java 11 file I/O APIs Adds a java11Test source set that compiles with --release 11 and runs only on JDK 11+. Tests cover the Java 11-only overloads that were instrumented but previously untestable from Java 8 sources: - FileReader(String/File, Charset) constructors - FileWriter(String/File, Charset[, boolean]) constructors - Files.writeString(Path, CharSequence, [Charset,] OpenOption...) - Files.readString(Path[, Charset]) - Path.of(String, String[]) and Path.of(URI) static methods Build configuration uses ext.java11TestMinJavaVersionForTests so the testJvmConstraints plugin skips the suite on pre-11 JVMs. * Remove unused StandardCopyOption import from FilesCallSiteTest * fix(appsec): gate FileChannel write callback on write-capable open options FileChannel.open() with READ-only options was incorrectly triggering the fileWritten callback, which could cause false positives in the zipslip rule (dog-920-110) when a read-only channel open with a traversal path coincided with a multipart zip upload in the same request. Split beforeOpen into two overload-specific methods so the OpenOption arguments can be inspected at the call site, mirroring the existing pattern in beforeRandomAccessFileOpened. Also fix a latent bug in AdviceGeneratorImpl: .sorted() without a comparator on ArgumentSpecification (which does not implement Comparable) would ClassCastException when an advice method captures a strict subset of a target method's arguments. Fixed with Comparator.comparingInt. * fix(appsec): capture all FileChannel.open args to avoid CSI generator partial-arg path beforeOpenSet previously captured only 2 of 3 arguments, triggering the partial-argument code path in AdviceGeneratorImpl which calls Stream.sorted() without a comparator on ArgumentSpecification (not Comparable). Adding the unused FileAttribute[] third parameter makes the capture complete and sequential, so isPositionalArguments() returns false and the sorted() path is never entered.
1 parent 081af53 commit 16f5eb8

35 files changed

Lines changed: 1357 additions & 0 deletions

dd-java-agent/instrumentation/java/java-io-1.8/build.gradle

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,20 @@ apply from: "$rootDir/gradle/java.gradle"
88
apply plugin: 'dd-trace-java.call-site-instrumentation'
99

1010
addTestSuiteForDir('latestDepTest', 'test')
11+
addTestSuiteForDir('java11Test', 'java11Test')
12+
13+
// java11Test only runs on JDK 11+; the testJvmConstraints plugin reads this property by convention
14+
ext.java11TestMinJavaVersionForTests = JavaVersion.VERSION_11
15+
16+
tasks.named("compileJava11TestJava", JavaCompile) {
17+
configureCompiler(it, 11)
18+
}
19+
tasks.named("compileJava11TestGroovy", GroovyCompile) {
20+
configureCompiler(it, 11)
21+
}
1122

1223
dependencies {
1324
testRuntimeOnly project(':dd-java-agent:instrumentation:datadog:asm:iast-instrumenter')
1425
testImplementation group: 'org.apache.tomcat', name: 'tomcat-catalina', version: '9.0.56'
26+
java11TestImplementation sourceSets.test.output
1527
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package datadog.trace.instrumentation.java.io
2+
3+
import datadog.trace.instrumentation.java.lang.FileIORaspHelper
4+
import foo.bar.TestFileReaderCharsetSuite
5+
6+
import java.nio.charset.Charset
7+
8+
class FileReaderCharsetCallSiteTest extends BaseIoRaspCallSiteTest {
9+
10+
void 'test RASP new FileReader with String path and Charset'() {
11+
setup:
12+
final helper = Mock(FileIORaspHelper)
13+
FileIORaspHelper.INSTANCE = helper
14+
final file = newFile('test_rasp_fr_str_cs.txt')
15+
16+
when:
17+
TestFileReaderCharsetSuite.newFileReader(file.path, Charset.defaultCharset()).close()
18+
19+
then:
20+
1 * helper.beforeFileLoaded(file.path)
21+
}
22+
23+
void 'test RASP new FileReader with File and Charset'() {
24+
setup:
25+
final helper = Mock(FileIORaspHelper)
26+
FileIORaspHelper.INSTANCE = helper
27+
final file = newFile('test_rasp_fr_file_cs.txt')
28+
29+
when:
30+
TestFileReaderCharsetSuite.newFileReader(file, Charset.defaultCharset()).close()
31+
32+
then:
33+
1 * helper.beforeFileLoaded(file.path)
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package datadog.trace.instrumentation.java.io
2+
3+
import datadog.trace.instrumentation.java.lang.FileIORaspHelper
4+
import foo.bar.TestFileWriterCharsetSuite
5+
6+
import java.nio.charset.Charset
7+
8+
class FileWriterCharsetCallSiteTest extends BaseIoRaspCallSiteTest {
9+
10+
void 'test RASP new FileWriter with String path and Charset'() {
11+
setup:
12+
final helper = Mock(FileIORaspHelper)
13+
FileIORaspHelper.INSTANCE = helper
14+
final file = newFile('test_rasp_fw_str_cs.txt')
15+
16+
when:
17+
TestFileWriterCharsetSuite.newFileWriter(file.path, Charset.defaultCharset()).close()
18+
19+
then:
20+
1 * helper.beforeFileWritten(file.path)
21+
}
22+
23+
void 'test RASP new FileWriter with String path, Charset, and append flag'() {
24+
setup:
25+
final helper = Mock(FileIORaspHelper)
26+
FileIORaspHelper.INSTANCE = helper
27+
final file = newFile('test_rasp_fw_str_cs_append.txt')
28+
29+
when:
30+
TestFileWriterCharsetSuite.newFileWriter(file.path, Charset.defaultCharset(), false).close()
31+
32+
then:
33+
1 * helper.beforeFileWritten(file.path)
34+
}
35+
36+
void 'test RASP new FileWriter with File and Charset'() {
37+
setup:
38+
final helper = Mock(FileIORaspHelper)
39+
FileIORaspHelper.INSTANCE = helper
40+
final file = newFile('test_rasp_fw_file_cs.txt')
41+
42+
when:
43+
TestFileWriterCharsetSuite.newFileWriter(file, Charset.defaultCharset()).close()
44+
45+
then:
46+
1 * helper.beforeFileWritten(file.path)
47+
}
48+
49+
void 'test RASP new FileWriter with File, Charset, and append flag'() {
50+
setup:
51+
final helper = Mock(FileIORaspHelper)
52+
FileIORaspHelper.INSTANCE = helper
53+
final file = newFile('test_rasp_fw_file_cs_append.txt')
54+
55+
when:
56+
TestFileWriterCharsetSuite.newFileWriter(file, Charset.defaultCharset(), false).close()
57+
58+
then:
59+
1 * helper.beforeFileWritten(file.path)
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package datadog.trace.instrumentation.java.io
2+
3+
import datadog.trace.instrumentation.java.lang.FileIORaspHelper
4+
import foo.bar.TestFilesJava11Suite
5+
6+
import java.nio.charset.StandardCharsets
7+
8+
class FilesJava11CallSiteTest extends BaseIoRaspCallSiteTest {
9+
10+
void 'test RASP Files.writeString without charset'() {
11+
setup:
12+
final helper = Mock(FileIORaspHelper)
13+
FileIORaspHelper.INSTANCE = helper
14+
final path = temporaryFolder.resolve('test_rasp_writestring.txt')
15+
16+
when:
17+
TestFilesJava11Suite.writeString(path, 'hello')
18+
19+
then:
20+
1 * helper.beforeFileWritten(path.toString())
21+
}
22+
23+
void 'test RASP Files.writeString with charset'() {
24+
setup:
25+
final helper = Mock(FileIORaspHelper)
26+
FileIORaspHelper.INSTANCE = helper
27+
final path = temporaryFolder.resolve('test_rasp_writestring_cs.txt')
28+
29+
when:
30+
TestFilesJava11Suite.writeString(path, 'hello', StandardCharsets.UTF_8)
31+
32+
then:
33+
1 * helper.beforeFileWritten(path.toString())
34+
}
35+
36+
void 'test RASP Files.readString without charset'() {
37+
setup:
38+
final helper = Mock(FileIORaspHelper)
39+
FileIORaspHelper.INSTANCE = helper
40+
final path = newFile('test_rasp_readstring.txt').toPath()
41+
42+
when:
43+
TestFilesJava11Suite.readString(path)
44+
45+
then:
46+
1 * helper.beforeFileLoaded(path.toString())
47+
}
48+
49+
void 'test RASP Files.readString with charset'() {
50+
setup:
51+
final helper = Mock(FileIORaspHelper)
52+
FileIORaspHelper.INSTANCE = helper
53+
final path = newFile('test_rasp_readstring_cs.txt').toPath()
54+
55+
when:
56+
TestFilesJava11Suite.readString(path, StandardCharsets.UTF_8)
57+
58+
then:
59+
1 * helper.beforeFileLoaded(path.toString())
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package datadog.trace.instrumentation.java.io
2+
3+
import datadog.trace.instrumentation.java.lang.FileIORaspHelper
4+
import foo.bar.TestPathOfSuite
5+
6+
class PathOfCallSiteTest extends BaseIoRaspCallSiteTest {
7+
8+
void 'test RASP Path.of from strings'(final String first, final String... more) {
9+
setup:
10+
final helper = Mock(FileIORaspHelper)
11+
FileIORaspHelper.INSTANCE = helper
12+
13+
when:
14+
TestPathOfSuite.of(first, more)
15+
16+
then:
17+
1 * helper.beforeFileLoaded(first, more)
18+
19+
where:
20+
first | more
21+
'test.txt' | [] as String[]
22+
'/tmp' | ['log', 'test.txt'] as String[]
23+
}
24+
25+
void 'test RASP Path.of from URI'() {
26+
setup:
27+
final helper = Mock(FileIORaspHelper)
28+
FileIORaspHelper.INSTANCE = helper
29+
final uri = new URI('file:/test.txt')
30+
31+
when:
32+
TestPathOfSuite.of(uri)
33+
34+
then:
35+
1 * helper.beforeFileLoaded(uri)
36+
}
37+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package foo.bar;
2+
3+
import java.io.File;
4+
import java.io.FileReader;
5+
import java.io.IOException;
6+
import java.nio.charset.Charset;
7+
8+
public class TestFileReaderCharsetSuite {
9+
10+
public static FileReader newFileReader(final String path, final Charset charset)
11+
throws IOException {
12+
return new FileReader(path, charset);
13+
}
14+
15+
public static FileReader newFileReader(final File file, final Charset charset)
16+
throws IOException {
17+
return new FileReader(file, charset);
18+
}
19+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package foo.bar;
2+
3+
import java.io.File;
4+
import java.io.FileWriter;
5+
import java.io.IOException;
6+
import java.nio.charset.Charset;
7+
8+
public class TestFileWriterCharsetSuite {
9+
10+
public static FileWriter newFileWriter(final String path, final Charset charset)
11+
throws IOException {
12+
return new FileWriter(path, charset);
13+
}
14+
15+
public static FileWriter newFileWriter(
16+
final String path, final Charset charset, final boolean append) throws IOException {
17+
return new FileWriter(path, charset, append);
18+
}
19+
20+
public static FileWriter newFileWriter(final File file, final Charset charset)
21+
throws IOException {
22+
return new FileWriter(file, charset);
23+
}
24+
25+
public static FileWriter newFileWriter(
26+
final File file, final Charset charset, final boolean append) throws IOException {
27+
return new FileWriter(file, charset, append);
28+
}
29+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package foo.bar;
2+
3+
import java.io.IOException;
4+
import java.nio.charset.Charset;
5+
import java.nio.file.Files;
6+
import java.nio.file.OpenOption;
7+
import java.nio.file.Path;
8+
9+
public class TestFilesJava11Suite {
10+
11+
public static Path writeString(
12+
final Path path, final CharSequence content, final OpenOption... options) throws IOException {
13+
return Files.writeString(path, content, options);
14+
}
15+
16+
public static Path writeString(
17+
final Path path,
18+
final CharSequence content,
19+
final Charset charset,
20+
final OpenOption... options)
21+
throws IOException {
22+
return Files.writeString(path, content, charset, options);
23+
}
24+
25+
public static String readString(final Path path) throws IOException {
26+
return Files.readString(path);
27+
}
28+
29+
public static String readString(final Path path, final Charset charset) throws IOException {
30+
return Files.readString(path, charset);
31+
}
32+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package foo.bar;
2+
3+
import java.net.URI;
4+
import java.nio.file.Path;
5+
6+
public class TestPathOfSuite {
7+
8+
public static Path of(final String first, final String... more) {
9+
return Path.of(first, more);
10+
}
11+
12+
public static Path of(final URI uri) {
13+
return Path.of(uri);
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package datadog.trace.instrumentation.java.lang;
2+
3+
import datadog.trace.agent.tooling.csi.CallSite;
4+
import datadog.trace.api.appsec.RaspCallSites;
5+
import java.nio.file.OpenOption;
6+
import java.nio.file.Path;
7+
import java.nio.file.StandardOpenOption;
8+
import java.nio.file.attribute.FileAttribute;
9+
import java.util.Set;
10+
import javax.annotation.Nullable;
11+
12+
@CallSite(
13+
spi = {RaspCallSites.class},
14+
helpers = FileIORaspHelper.class)
15+
public class FileChannelCallSite {
16+
17+
@CallSite.Before(
18+
"java.nio.channels.FileChannel java.nio.channels.FileChannel.open(java.nio.file.Path, java.nio.file.OpenOption[])")
19+
public static void beforeOpenArray(
20+
@CallSite.Argument(0) @Nullable final Path path,
21+
@CallSite.Argument(1) @Nullable final OpenOption[] options) {
22+
if (path != null) {
23+
String pathStr = path.toString();
24+
FileIORaspHelper.INSTANCE.beforeFileLoaded(pathStr);
25+
if (hasWriteOption(options)) {
26+
FileIORaspHelper.INSTANCE.beforeFileWritten(pathStr);
27+
}
28+
}
29+
}
30+
31+
@CallSite.Before(
32+
"java.nio.channels.FileChannel java.nio.channels.FileChannel.open(java.nio.file.Path, java.util.Set, java.nio.file.attribute.FileAttribute[])")
33+
public static void beforeOpenSet(
34+
@CallSite.Argument(0) @Nullable final Path path,
35+
@CallSite.Argument(1) @Nullable final Set<? extends OpenOption> options,
36+
@CallSite.Argument(2) @Nullable final FileAttribute<?>[] attrs) {
37+
if (path != null) {
38+
String pathStr = path.toString();
39+
FileIORaspHelper.INSTANCE.beforeFileLoaded(pathStr);
40+
if (hasWriteOption(options)) {
41+
FileIORaspHelper.INSTANCE.beforeFileWritten(pathStr);
42+
}
43+
}
44+
}
45+
46+
private static boolean hasWriteOption(@Nullable final OpenOption[] options) {
47+
if (options == null) {
48+
return false;
49+
}
50+
for (OpenOption opt : options) {
51+
if (isWriteOption(opt)) {
52+
return true;
53+
}
54+
}
55+
return false;
56+
}
57+
58+
private static boolean hasWriteOption(@Nullable final Set<? extends OpenOption> options) {
59+
if (options == null) {
60+
return false;
61+
}
62+
for (OpenOption opt : options) {
63+
if (isWriteOption(opt)) {
64+
return true;
65+
}
66+
}
67+
return false;
68+
}
69+
70+
private static boolean isWriteOption(final OpenOption opt) {
71+
return opt == StandardOpenOption.WRITE
72+
|| opt == StandardOpenOption.APPEND
73+
|| opt == StandardOpenOption.CREATE
74+
|| opt == StandardOpenOption.CREATE_NEW
75+
|| opt == StandardOpenOption.TRUNCATE_EXISTING;
76+
}
77+
}

0 commit comments

Comments
 (0)