Skip to content

Commit da8bdd2

Browse files
jandro996devflow.devflow-routing-intake
andauthored
feat(appsec): expose server.io.fs.file_write address for write file operations (#11084)
feat(appsec): expose server.io.fs.file_write address for write file operations FileOutputStream call sites now publish server.io.fs.file_write instead of server.io.fs.file, allowing detection rules to distinguish between read and write operations. Adds the dog-920-110 Zipslip rule that uses the new address. refactor: rename FileLoadedRaspHelper to FileIORaspHelper The class now handles both read and write file operations so the old name was misleading. FileIORaspHelper better reflects its responsibility. fix(appsec): publish server.io.fs.file alongside server.io.fs.file_write on writes File write events now populate both addresses so that existing rules using server.io.fs.file continue to fire for write operations, while new rules can use server.io.fs.file_write to target writes specifically. Fix CI test failures for file_write RASP event - Add FILE_WRITTEN_ID to InstrumentationGateway callback-wrapping switch so exceptions in fileWritten() callbacks are properly caught (fixes InstrumentationGatewayTest#testThrowableBlocking) - Change rasp-930-101 smoke test rule from lfi_detector to match_regex operator, since lfi_detector only supports server.io.fs.file as resource address; match_regex on server.io.fs.file_write with path-traversal regex correctly detects ../../../etc/passwd patterns ci: retrigger pipeline ci: retrigger pipeline fix(appsec-smoke): use lfi_detector for server.io.fs.file_write test rule match_regex is a WAF operator not evaluated in RASP ephemeral mode. Switch rasp-930-101 back to lfi_detector with server.io.fs.file_write as resource — lfi_detector is a RASP operator that works in ephemeral mode and accepts any string address as the file path resource. fix(appsec-smoke): simplify LFI write test to use rasp-930-100 trigger server.io.fs.file_write is a new address not yet registered in the ddwaf binary as a RASP ephemeral address, so WAF rules using it as a trigger are not evaluated in RASP mode. The smoke test now verifies that FileOutputStream write operations are intercepted and blocked by RASP via the backwards-compat server.io.fs.file address (rasp-930-100), which is the correct behaviour given the current ddwaf version. Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent 16f8277 commit da8bdd2

File tree

22 files changed

+271
-55
lines changed

22 files changed

+271
-55
lines changed

dd-java-agent/appsec/src/main/java/com/datadog/appsec/event/data/KnownAddresses.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ public interface KnownAddresses {
141141
/** The representation of opened file on the filesystem */
142142
Address<String> IO_FS_FILE = new Address<>("server.io.fs.file");
143143

144+
/** The representation of a file being written on the filesystem */
145+
Address<String> IO_FS_FILE_WRITE = new Address<>("server.io.fs.file_write");
146+
144147
/** The database type (ex: mysql, postgresql, sqlite) */
145148
Address<String> DB_TYPE = new Address<>("server.db.system");
146149

@@ -240,6 +243,8 @@ static Address<?> forName(String name) {
240243
return IO_NET_RESPONSE_BODY;
241244
case "server.io.fs.file":
242245
return IO_FS_FILE;
246+
case "server.io.fs.file_write":
247+
return IO_FS_FILE_WRITE;
243248
case "server.db.system":
244249
return DB_TYPE;
245250
case "server.db.statement":

dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ public class GatewayBridge {
123123
private volatile DataSubscriberInfo httpClientRequestSubInfo;
124124
private volatile DataSubscriberInfo httpClientResponseSubInfo;
125125
private volatile DataSubscriberInfo ioFileSubInfo;
126+
private volatile DataSubscriberInfo ioFileWriteSubInfo;
126127
private volatile DataSubscriberInfo sessionIdSubInfo;
127128
private volatile DataSubscriberInfo userIdSubInfo;
128129
private final ConcurrentHashMap<String, DataSubscriberInfo> loginEventSubInfo =
@@ -188,6 +189,7 @@ public void init() {
188189
subscriptionService.registerCallback(EVENTS.httpClientRequest(), this::onHttpClientRequest);
189190
subscriptionService.registerCallback(EVENTS.httpClientResponse(), this::onHttpClientResponse);
190191
subscriptionService.registerCallback(EVENTS.fileLoaded(), this::onFileLoaded);
192+
subscriptionService.registerCallback(EVENTS.fileWritten(), this::onFileWritten);
191193
subscriptionService.registerCallback(EVENTS.requestSession(), this::onRequestSession);
192194
subscriptionService.registerCallback(EVENTS.execCmd(), this::onExecCmd);
193195
subscriptionService.registerCallback(EVENTS.shellCmd(), this::onShellCmd);
@@ -548,6 +550,36 @@ private Flow<Void> onFileLoaded(RequestContext ctx_, String path) {
548550
}
549551
}
550552

553+
private Flow<Void> onFileWritten(RequestContext ctx_, String path) {
554+
AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
555+
if (ctx == null) {
556+
return NoopFlow.INSTANCE;
557+
}
558+
while (true) {
559+
DataSubscriberInfo subInfo = ioFileWriteSubInfo;
560+
if (subInfo == null) {
561+
subInfo =
562+
producerService.getDataSubscribers(
563+
KnownAddresses.IO_FS_FILE, KnownAddresses.IO_FS_FILE_WRITE);
564+
ioFileWriteSubInfo = subInfo;
565+
}
566+
if (subInfo == null || subInfo.isEmpty()) {
567+
return NoopFlow.INSTANCE;
568+
}
569+
DataBundle bundle =
570+
new MapDataBundle.Builder(CAPACITY_0_2)
571+
.add(KnownAddresses.IO_FS_FILE, path)
572+
.add(KnownAddresses.IO_FS_FILE_WRITE, path)
573+
.build();
574+
try {
575+
GatewayContext gwCtx = new GatewayContext(true, RuleType.LFI);
576+
return producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx);
577+
} catch (ExpiredSubscriberInfoException e) {
578+
ioFileWriteSubInfo = null;
579+
}
580+
}
581+
}
582+
551583
private Flow<Void> onRequestFilesFilenames(RequestContext ctx_, List<String> filenames) {
552584
AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
553585
if (ctx == null || filenames == null || filenames.isEmpty()) {

dd-java-agent/appsec/src/main/resources/default_config.json

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5457,6 +5457,75 @@
54575457
],
54585458
"transformers": []
54595459
},
5460+
{
5461+
"id": "dog-920-110",
5462+
"name": "Zipslip Attack - Unsafe Zip extraction",
5463+
"tags": {
5464+
"type": "http_protocol_violation",
5465+
"category": "attack_attempt",
5466+
"cwe": "502",
5467+
"capec": "1000/152/586",
5468+
"confidence": "0",
5469+
"module": "waf"
5470+
},
5471+
"conditions": [
5472+
{
5473+
"parameters": {
5474+
"inputs": [
5475+
{
5476+
"address": "server.request.body.filenames"
5477+
},
5478+
{
5479+
"address": "server.request.headers.no_cookies",
5480+
"key_path": [
5481+
"x-filename"
5482+
]
5483+
},
5484+
{
5485+
"address": "server.request.headers.no_cookies",
5486+
"key_path": [
5487+
"x_filename"
5488+
]
5489+
},
5490+
{
5491+
"address": "server.request.headers.no_cookies",
5492+
"key_path": [
5493+
"x.filename"
5494+
]
5495+
},
5496+
{
5497+
"address": "server.request.headers.no_cookies",
5498+
"key_path": [
5499+
"x-file-name"
5500+
]
5501+
}
5502+
],
5503+
"regex": "\\.zip$",
5504+
"options": {
5505+
"case_sensitive": true,
5506+
"min_length": 5
5507+
}
5508+
},
5509+
"operator": "match_regex"
5510+
},
5511+
{
5512+
"parameters": {
5513+
"inputs": [
5514+
{
5515+
"address": "server.io.fs.file_write"
5516+
}
5517+
],
5518+
"regex": "(?:^|[/\\\\])\\.\\.[/\\\\]",
5519+
"options": {
5520+
"case_sensitive": true,
5521+
"min_length": 4
5522+
}
5523+
},
5524+
"operator": "match_regex"
5525+
}
5526+
],
5527+
"transformers": []
5528+
},
54605529
{
54615530
"id": "dog-931-001",
54625531
"name": "RFI: URL Payload to well known RFI target",

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/event/data/KnownAddressesSpecificationForkedTest.groovy

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class KnownAddressesSpecificationForkedTest extends Specification {
4949
'server.io.net.response.headers',
5050
'server.io.net.response.body',
5151
'server.io.fs.file',
52+
'server.io.fs.file_write',
5253
'server.sys.exec.cmd',
5354
'server.sys.shell.cmd',
5455
'waf.context.processor'
@@ -57,7 +58,7 @@ class KnownAddressesSpecificationForkedTest extends Specification {
5758

5859
void 'number of known addresses is expected number'() {
5960
expect:
60-
Address.instanceCount() == 45
61+
Address.instanceCount() == 46
6162
KnownAddresses.WAF_CONTEXT_PROCESSOR.serial == Address.instanceCount() - 1
6263
}
6364
}

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ class GatewayBridgeSpecification extends DDSpecification {
121121
BiFunction<RequestContext, HttpClientResponse, Flow<Void>> httpClientResponseCB
122122
BiFunction<RequestContext, Long, Flow<Void>> httpClientSamplingCB
123123
BiFunction<RequestContext, String, Flow<Void>> fileLoadedCB
124+
BiFunction<RequestContext, String, Flow<Void>> fileWrittenCB
124125
BiFunction<RequestContext, List<String>, Flow<Void>> requestFilesFilenamesCB
125126
BiFunction<RequestContext, String, Flow<Void>> requestSessionCB
126127
BiFunction<RequestContext, String[], Flow<Void>> execCmdCB
@@ -536,6 +537,9 @@ class GatewayBridgeSpecification extends DDSpecification {
536537
1 * ig.registerCallback(EVENTS.fileLoaded(), _) >> {
537538
fileLoadedCB = it[1]; null
538539
}
540+
1 * ig.registerCallback(EVENTS.fileWritten(), _) >> {
541+
fileWrittenCB = it[1]; null
542+
}
539543
1 * ig.registerCallback(EVENTS.requestSession(), _) >> {
540544
requestSessionCB = it[1]; null
541545
}
@@ -1082,6 +1086,30 @@ class GatewayBridgeSpecification extends DDSpecification {
10821086
gatewayContext.isRasp == true
10831087
}
10841088

1089+
void 'process file written'() {
1090+
setup:
1091+
final path = '/tmp/output.txt'
1092+
eventDispatcher.getDataSubscribers({
1093+
KnownAddresses.IO_FS_FILE in it && KnownAddresses.IO_FS_FILE_WRITE in it
1094+
}) >> nonEmptyDsInfo
1095+
DataBundle bundle
1096+
GatewayContext gatewayContext
1097+
1098+
when:
1099+
Flow<?> flow = fileWrittenCB.apply(ctx, path)
1100+
1101+
then:
1102+
1 * eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >> {
1103+
a, b, db, gw -> bundle = db; gatewayContext = gw; NoopFlow.INSTANCE
1104+
}
1105+
bundle.get(KnownAddresses.IO_FS_FILE) == path
1106+
bundle.get(KnownAddresses.IO_FS_FILE_WRITE) == path
1107+
flow.result == null
1108+
flow.action == Flow.Action.Noop.INSTANCE
1109+
gatewayContext.isTransient == true
1110+
gatewayContext.isRasp == true
1111+
}
1112+
10851113
void 'process request files filenames'() {
10861114
setup:
10871115
final filenames = ['malicious.php', 'document.pdf']

dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileCallSite.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
@Sink(VulnerabilityTypes.PATH_TRAVERSAL)
1515
@CallSite(
1616
spi = {IastCallSites.class, RaspCallSites.class},
17-
helpers = FileLoadedRaspHelper.class)
17+
helpers = FileIORaspHelper.class)
1818
public class FileCallSite {
1919

2020
@CallSite.Before("void java.io.File.<init>(java.lang.String)")
@@ -101,18 +101,18 @@ private static void iastCallback(URI uri) {
101101
}
102102

103103
private static void raspCallback(File parent, String child) {
104-
FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(parent, child);
104+
FileIORaspHelper.INSTANCE.beforeFileLoaded(parent, child);
105105
}
106106

107107
private static void raspCallback(String parent, String file) {
108-
FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(parent, file);
108+
FileIORaspHelper.INSTANCE.beforeFileLoaded(parent, file);
109109
}
110110

111111
private static void raspCallback(String s) {
112-
FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(s);
112+
FileIORaspHelper.INSTANCE.beforeFileLoaded(s);
113113
}
114114

115115
private static void raspCallback(URI uri) {
116-
FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(uri);
116+
FileIORaspHelper.INSTANCE.beforeFileLoaded(uri);
117117
}
118118
}

dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileLoadedRaspHelper.java renamed to dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileIORaspHelper.java

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import datadog.appsec.api.blocking.BlockingException;
66
import datadog.trace.api.Config;
77
import datadog.trace.api.gateway.BlockResponseFunction;
8+
import datadog.trace.api.gateway.EventType;
89
import datadog.trace.api.gateway.Flow;
910
import datadog.trace.api.gateway.RequestContext;
1011
import datadog.trace.api.gateway.RequestContextSlot;
@@ -19,13 +20,13 @@
1920
import org.slf4j.Logger;
2021
import org.slf4j.LoggerFactory;
2122

22-
public class FileLoadedRaspHelper {
23+
public class FileIORaspHelper {
2324

24-
public static FileLoadedRaspHelper INSTANCE = new FileLoadedRaspHelper();
25+
public static FileIORaspHelper INSTANCE = new FileIORaspHelper();
2526

26-
private static final Logger LOGGER = LoggerFactory.getLogger(FileLoadedRaspHelper.class);
27+
private static final Logger LOGGER = LoggerFactory.getLogger(FileIORaspHelper.class);
2728

28-
private FileLoadedRaspHelper() {
29+
private FileIORaspHelper() {
2930
// prevent instantiation
3031
}
3132

@@ -93,16 +94,24 @@ public void beforeFileLoaded(@Nullable final File parent, @Nonnull final String
9394
}
9495

9596
public void beforeFileLoaded(@Nonnull final String path) {
97+
invokeRaspCallback(EVENTS.fileLoaded(), path);
98+
}
99+
100+
public void beforeFileWritten(@Nonnull final String path) {
101+
invokeRaspCallback(EVENTS.fileWritten(), path);
102+
}
103+
104+
private void invokeRaspCallback(
105+
EventType<BiFunction<RequestContext, String, Flow<Void>>> eventType,
106+
@Nonnull final String path) {
96107
if (!Config.get().isAppSecRaspEnabled()) {
97108
return;
98109
}
99110
try {
100-
final BiFunction<RequestContext, String, Flow<Void>> fileLoadedCallback =
101-
AgentTracer.get()
102-
.getCallbackProvider(RequestContextSlot.APPSEC)
103-
.getCallback(EVENTS.fileLoaded());
111+
final BiFunction<RequestContext, String, Flow<Void>> callback =
112+
AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC).getCallback(eventType);
104113

105-
if (fileLoadedCallback == null) {
114+
if (callback == null) {
106115
return;
107116
}
108117

@@ -116,7 +125,7 @@ public void beforeFileLoaded(@Nonnull final String path) {
116125
return;
117126
}
118127

119-
Flow<Void> flow = fileLoadedCallback.apply(ctx, path);
128+
Flow<Void> flow = callback.apply(ctx, path);
120129
Flow.Action action = flow.getAction();
121130
if (action instanceof Flow.Action.RequestBlockingAction) {
122131
BlockResponseFunction brf = ctx.getBlockResponseFunction();

dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileInputStreamCallSite.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
@Sink(VulnerabilityTypes.PATH_TRAVERSAL)
1313
@CallSite(
1414
spi = {IastCallSites.class, RaspCallSites.class},
15-
helpers = FileLoadedRaspHelper.class)
15+
helpers = FileIORaspHelper.class)
1616
public class FileInputStreamCallSite {
1717

1818
@CallSite.Before("void java.io.FileInputStream.<init>(java.lang.String)")
@@ -35,6 +35,6 @@ private static void iastCallback(String path) {
3535
}
3636

3737
private static void raspCallback(String path) {
38-
FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(path);
38+
FileIORaspHelper.INSTANCE.beforeFileLoaded(path);
3939
}
4040
}

dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileOutputStreamCallSite.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
@Sink(VulnerabilityTypes.PATH_TRAVERSAL)
1313
@CallSite(
1414
spi = {IastCallSites.class, RaspCallSites.class},
15-
helpers = FileLoadedRaspHelper.class)
15+
helpers = FileIORaspHelper.class)
1616
public class FileOutputStreamCallSite {
1717

1818
@CallSite.Before("void java.io.FileOutputStream.<init>(java.lang.String)")
@@ -36,6 +36,6 @@ private static void iastCallback(String path) {
3636
}
3737

3838
private static void raspCallback(String path) {
39-
FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(path);
39+
FileIORaspHelper.INSTANCE.beforeFileWritten(path);
4040
}
4141
}

dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/PathCallSite.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
@Sink(VulnerabilityTypes.PATH_TRAVERSAL)
1313
@CallSite(
1414
spi = {IastCallSites.class, RaspCallSites.class},
15-
helpers = FileLoadedRaspHelper.class)
15+
helpers = FileIORaspHelper.class)
1616
public class PathCallSite {
1717

1818
@CallSite.Before("java.nio.file.Path java.nio.file.Path.resolve(java.lang.String)")
@@ -36,6 +36,6 @@ private static void iastCallback(String other) {
3636
}
3737

3838
private static void raspCallback(String other) {
39-
FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(other);
39+
FileIORaspHelper.INSTANCE.beforeFileLoaded(other);
4040
}
4141
}

0 commit comments

Comments
 (0)