diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/event/data/KnownAddresses.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/event/data/KnownAddresses.java index 36c52f2617b..4a5a66e0074 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/event/data/KnownAddresses.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/event/data/KnownAddresses.java @@ -141,6 +141,9 @@ public interface KnownAddresses { /** The representation of opened file on the filesystem */ Address IO_FS_FILE = new Address<>("server.io.fs.file"); + /** The representation of a file being written on the filesystem */ + Address IO_FS_FILE_WRITE = new Address<>("server.io.fs.file_write"); + /** The database type (ex: mysql, postgresql, sqlite) */ Address DB_TYPE = new Address<>("server.db.system"); @@ -240,6 +243,8 @@ static Address forName(String name) { return IO_NET_RESPONSE_BODY; case "server.io.fs.file": return IO_FS_FILE; + case "server.io.fs.file_write": + return IO_FS_FILE_WRITE; case "server.db.system": return DB_TYPE; case "server.db.statement": diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java index 06b0bbd7888..669b6de7dd9 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java @@ -123,6 +123,7 @@ public class GatewayBridge { private volatile DataSubscriberInfo httpClientRequestSubInfo; private volatile DataSubscriberInfo httpClientResponseSubInfo; private volatile DataSubscriberInfo ioFileSubInfo; + private volatile DataSubscriberInfo ioFileWriteSubInfo; private volatile DataSubscriberInfo sessionIdSubInfo; private volatile DataSubscriberInfo userIdSubInfo; private final ConcurrentHashMap loginEventSubInfo = @@ -188,6 +189,7 @@ public void init() { subscriptionService.registerCallback(EVENTS.httpClientRequest(), this::onHttpClientRequest); subscriptionService.registerCallback(EVENTS.httpClientResponse(), this::onHttpClientResponse); subscriptionService.registerCallback(EVENTS.fileLoaded(), this::onFileLoaded); + subscriptionService.registerCallback(EVENTS.fileWritten(), this::onFileWritten); subscriptionService.registerCallback(EVENTS.requestSession(), this::onRequestSession); subscriptionService.registerCallback(EVENTS.execCmd(), this::onExecCmd); subscriptionService.registerCallback(EVENTS.shellCmd(), this::onShellCmd); @@ -548,6 +550,36 @@ private Flow onFileLoaded(RequestContext ctx_, String path) { } } + private Flow onFileWritten(RequestContext ctx_, String path) { + AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC); + if (ctx == null) { + return NoopFlow.INSTANCE; + } + while (true) { + DataSubscriberInfo subInfo = ioFileWriteSubInfo; + if (subInfo == null) { + subInfo = + producerService.getDataSubscribers( + KnownAddresses.IO_FS_FILE, KnownAddresses.IO_FS_FILE_WRITE); + ioFileWriteSubInfo = subInfo; + } + if (subInfo == null || subInfo.isEmpty()) { + return NoopFlow.INSTANCE; + } + DataBundle bundle = + new MapDataBundle.Builder(CAPACITY_0_2) + .add(KnownAddresses.IO_FS_FILE, path) + .add(KnownAddresses.IO_FS_FILE_WRITE, path) + .build(); + try { + GatewayContext gwCtx = new GatewayContext(true, RuleType.LFI); + return producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx); + } catch (ExpiredSubscriberInfoException e) { + ioFileWriteSubInfo = null; + } + } + } + private Flow onRequestFilesFilenames(RequestContext ctx_, List filenames) { AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC); if (ctx == null || filenames == null || filenames.isEmpty()) { diff --git a/dd-java-agent/appsec/src/main/resources/default_config.json b/dd-java-agent/appsec/src/main/resources/default_config.json index 2f53276c7e5..81936a642c5 100644 --- a/dd-java-agent/appsec/src/main/resources/default_config.json +++ b/dd-java-agent/appsec/src/main/resources/default_config.json @@ -5457,6 +5457,75 @@ ], "transformers": [] }, + { + "id": "dog-920-110", + "name": "Zipslip Attack - Unsafe Zip extraction", + "tags": { + "type": "http_protocol_violation", + "category": "attack_attempt", + "cwe": "502", + "capec": "1000/152/586", + "confidence": "0", + "module": "waf" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.body.filenames" + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "x-filename" + ] + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "x_filename" + ] + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "x.filename" + ] + }, + { + "address": "server.request.headers.no_cookies", + "key_path": [ + "x-file-name" + ] + } + ], + "regex": "\\.zip$", + "options": { + "case_sensitive": true, + "min_length": 5 + } + }, + "operator": "match_regex" + }, + { + "parameters": { + "inputs": [ + { + "address": "server.io.fs.file_write" + } + ], + "regex": "(?:^|[/\\\\])\\.\\.[/\\\\]", + "options": { + "case_sensitive": true, + "min_length": 4 + } + }, + "operator": "match_regex" + } + ], + "transformers": [] + }, { "id": "dog-931-001", "name": "RFI: URL Payload to well known RFI target", diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/event/data/KnownAddressesSpecificationForkedTest.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/event/data/KnownAddressesSpecificationForkedTest.groovy index 432363f9073..06a1a61799d 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/event/data/KnownAddressesSpecificationForkedTest.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/event/data/KnownAddressesSpecificationForkedTest.groovy @@ -49,6 +49,7 @@ class KnownAddressesSpecificationForkedTest extends Specification { 'server.io.net.response.headers', 'server.io.net.response.body', 'server.io.fs.file', + 'server.io.fs.file_write', 'server.sys.exec.cmd', 'server.sys.shell.cmd', 'waf.context.processor' @@ -57,7 +58,7 @@ class KnownAddressesSpecificationForkedTest extends Specification { void 'number of known addresses is expected number'() { expect: - Address.instanceCount() == 45 + Address.instanceCount() == 46 KnownAddresses.WAF_CONTEXT_PROCESSOR.serial == Address.instanceCount() - 1 } } diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy index 33d1f8c946c..505c4950c22 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy @@ -121,6 +121,7 @@ class GatewayBridgeSpecification extends DDSpecification { BiFunction> httpClientResponseCB BiFunction> httpClientSamplingCB BiFunction> fileLoadedCB + BiFunction> fileWrittenCB BiFunction, Flow> requestFilesFilenamesCB BiFunction> requestSessionCB BiFunction> execCmdCB @@ -536,6 +537,9 @@ class GatewayBridgeSpecification extends DDSpecification { 1 * ig.registerCallback(EVENTS.fileLoaded(), _) >> { fileLoadedCB = it[1]; null } + 1 * ig.registerCallback(EVENTS.fileWritten(), _) >> { + fileWrittenCB = it[1]; null + } 1 * ig.registerCallback(EVENTS.requestSession(), _) >> { requestSessionCB = it[1]; null } @@ -1082,6 +1086,30 @@ class GatewayBridgeSpecification extends DDSpecification { gatewayContext.isRasp == true } + void 'process file written'() { + setup: + final path = '/tmp/output.txt' + eventDispatcher.getDataSubscribers({ + KnownAddresses.IO_FS_FILE in it && KnownAddresses.IO_FS_FILE_WRITE in it + }) >> nonEmptyDsInfo + DataBundle bundle + GatewayContext gatewayContext + + when: + Flow flow = fileWrittenCB.apply(ctx, path) + + then: + 1 * eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >> { + a, b, db, gw -> bundle = db; gatewayContext = gw; NoopFlow.INSTANCE + } + bundle.get(KnownAddresses.IO_FS_FILE) == path + bundle.get(KnownAddresses.IO_FS_FILE_WRITE) == path + flow.result == null + flow.action == Flow.Action.Noop.INSTANCE + gatewayContext.isTransient == true + gatewayContext.isRasp == true + } + void 'process request files filenames'() { setup: final filenames = ['malicious.php', 'document.pdf'] diff --git a/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileCallSite.java b/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileCallSite.java index 0a082a50045..c0b49d2fbac 100644 --- a/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileCallSite.java +++ b/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileCallSite.java @@ -14,7 +14,7 @@ @Sink(VulnerabilityTypes.PATH_TRAVERSAL) @CallSite( spi = {IastCallSites.class, RaspCallSites.class}, - helpers = FileLoadedRaspHelper.class) + helpers = FileIORaspHelper.class) public class FileCallSite { @CallSite.Before("void java.io.File.(java.lang.String)") @@ -101,18 +101,18 @@ private static void iastCallback(URI uri) { } private static void raspCallback(File parent, String child) { - FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(parent, child); + FileIORaspHelper.INSTANCE.beforeFileLoaded(parent, child); } private static void raspCallback(String parent, String file) { - FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(parent, file); + FileIORaspHelper.INSTANCE.beforeFileLoaded(parent, file); } private static void raspCallback(String s) { - FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(s); + FileIORaspHelper.INSTANCE.beforeFileLoaded(s); } private static void raspCallback(URI uri) { - FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(uri); + FileIORaspHelper.INSTANCE.beforeFileLoaded(uri); } } diff --git a/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileLoadedRaspHelper.java b/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileIORaspHelper.java similarity index 83% rename from dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileLoadedRaspHelper.java rename to dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileIORaspHelper.java index a73ce5640b1..c3b48919977 100644 --- a/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileLoadedRaspHelper.java +++ b/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileIORaspHelper.java @@ -5,6 +5,7 @@ import datadog.appsec.api.blocking.BlockingException; import datadog.trace.api.Config; import datadog.trace.api.gateway.BlockResponseFunction; +import datadog.trace.api.gateway.EventType; import datadog.trace.api.gateway.Flow; import datadog.trace.api.gateway.RequestContext; import datadog.trace.api.gateway.RequestContextSlot; @@ -19,13 +20,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class FileLoadedRaspHelper { +public class FileIORaspHelper { - public static FileLoadedRaspHelper INSTANCE = new FileLoadedRaspHelper(); + public static FileIORaspHelper INSTANCE = new FileIORaspHelper(); - private static final Logger LOGGER = LoggerFactory.getLogger(FileLoadedRaspHelper.class); + private static final Logger LOGGER = LoggerFactory.getLogger(FileIORaspHelper.class); - private FileLoadedRaspHelper() { + private FileIORaspHelper() { // prevent instantiation } @@ -93,16 +94,24 @@ public void beforeFileLoaded(@Nullable final File parent, @Nonnull final String } public void beforeFileLoaded(@Nonnull final String path) { + invokeRaspCallback(EVENTS.fileLoaded(), path); + } + + public void beforeFileWritten(@Nonnull final String path) { + invokeRaspCallback(EVENTS.fileWritten(), path); + } + + private void invokeRaspCallback( + EventType>> eventType, + @Nonnull final String path) { if (!Config.get().isAppSecRaspEnabled()) { return; } try { - final BiFunction> fileLoadedCallback = - AgentTracer.get() - .getCallbackProvider(RequestContextSlot.APPSEC) - .getCallback(EVENTS.fileLoaded()); + final BiFunction> callback = + AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC).getCallback(eventType); - if (fileLoadedCallback == null) { + if (callback == null) { return; } @@ -116,7 +125,7 @@ public void beforeFileLoaded(@Nonnull final String path) { return; } - Flow flow = fileLoadedCallback.apply(ctx, path); + Flow flow = callback.apply(ctx, path); Flow.Action action = flow.getAction(); if (action instanceof Flow.Action.RequestBlockingAction) { BlockResponseFunction brf = ctx.getBlockResponseFunction(); diff --git a/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileInputStreamCallSite.java b/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileInputStreamCallSite.java index 6c53f0c1ee4..4c293efb859 100644 --- a/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileInputStreamCallSite.java +++ b/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileInputStreamCallSite.java @@ -12,7 +12,7 @@ @Sink(VulnerabilityTypes.PATH_TRAVERSAL) @CallSite( spi = {IastCallSites.class, RaspCallSites.class}, - helpers = FileLoadedRaspHelper.class) + helpers = FileIORaspHelper.class) public class FileInputStreamCallSite { @CallSite.Before("void java.io.FileInputStream.(java.lang.String)") @@ -35,6 +35,6 @@ private static void iastCallback(String path) { } private static void raspCallback(String path) { - FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(path); + FileIORaspHelper.INSTANCE.beforeFileLoaded(path); } } diff --git a/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileOutputStreamCallSite.java b/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileOutputStreamCallSite.java index 070ed25ac43..478a4ec795e 100644 --- a/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileOutputStreamCallSite.java +++ b/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/FileOutputStreamCallSite.java @@ -12,7 +12,7 @@ @Sink(VulnerabilityTypes.PATH_TRAVERSAL) @CallSite( spi = {IastCallSites.class, RaspCallSites.class}, - helpers = FileLoadedRaspHelper.class) + helpers = FileIORaspHelper.class) public class FileOutputStreamCallSite { @CallSite.Before("void java.io.FileOutputStream.(java.lang.String)") @@ -36,6 +36,6 @@ private static void iastCallback(String path) { } private static void raspCallback(String path) { - FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(path); + FileIORaspHelper.INSTANCE.beforeFileWritten(path); } } diff --git a/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/PathCallSite.java b/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/PathCallSite.java index 9d6959dce5c..19015bed8b0 100644 --- a/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/PathCallSite.java +++ b/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/PathCallSite.java @@ -12,7 +12,7 @@ @Sink(VulnerabilityTypes.PATH_TRAVERSAL) @CallSite( spi = {IastCallSites.class, RaspCallSites.class}, - helpers = FileLoadedRaspHelper.class) + helpers = FileIORaspHelper.class) public class PathCallSite { @CallSite.Before("java.nio.file.Path java.nio.file.Path.resolve(java.lang.String)") @@ -36,6 +36,6 @@ private static void iastCallback(String other) { } private static void raspCallback(String other) { - FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(other); + FileIORaspHelper.INSTANCE.beforeFileLoaded(other); } } diff --git a/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/PathsCallSite.java b/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/PathsCallSite.java index 70dafd2e1b3..8bc531d3f77 100644 --- a/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/PathsCallSite.java +++ b/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/PathsCallSite.java @@ -13,7 +13,7 @@ @Sink(VulnerabilityTypes.PATH_TRAVERSAL) @CallSite( spi = {IastCallSites.class, RaspCallSites.class}, - helpers = FileLoadedRaspHelper.class) + helpers = FileIORaspHelper.class) public class PathsCallSite { @CallSite.Before( @@ -58,10 +58,10 @@ private static void iastCallback(String first, String[] more) { } private static void raspCallback(String first, String[] more) { - FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(first, more); + FileIORaspHelper.INSTANCE.beforeFileLoaded(first, more); } private static void raspCallback(URI uri) { - FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(uri); + FileIORaspHelper.INSTANCE.beforeFileLoaded(uri); } } diff --git a/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileCallSiteTest.groovy b/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileCallSiteTest.groovy index ed3ff70b442..4256ed45d5f 100644 --- a/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileCallSiteTest.groovy +++ b/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileCallSiteTest.groovy @@ -4,7 +4,7 @@ import datadog.trace.api.gateway.CallbackProvider import datadog.trace.api.gateway.RequestContextSlot import datadog.trace.api.iast.InstrumentationBridge import datadog.trace.api.iast.sink.PathTraversalModule -import datadog.trace.instrumentation.java.lang.FileLoadedRaspHelper +import datadog.trace.instrumentation.java.lang.FileIORaspHelper import foo.bar.TestFileSuite import java.util.function.BiFunction @@ -84,8 +84,8 @@ class FileCallSiteTest extends BaseIoRaspCallSiteTest { void 'test RASP new file with parent and child'() { setup: - final helper = Mock(FileLoadedRaspHelper) - FileLoadedRaspHelper.INSTANCE = helper + final helper = Mock(FileIORaspHelper) + FileIORaspHelper.INSTANCE = helper final parent = '/home/test' final child = 'test.txt' @@ -98,8 +98,8 @@ class FileCallSiteTest extends BaseIoRaspCallSiteTest { void 'test RASP new file with parent file and child'() { setup: - final helper = Mock(FileLoadedRaspHelper) - FileLoadedRaspHelper.INSTANCE = helper + final helper = Mock(FileIORaspHelper) + FileIORaspHelper.INSTANCE = helper final parent = new File('/home/test') final child = 'test.txt' @@ -112,8 +112,8 @@ class FileCallSiteTest extends BaseIoRaspCallSiteTest { void 'test RASP new file with uri'() { setup: - final helper = Mock(FileLoadedRaspHelper) - FileLoadedRaspHelper.INSTANCE = helper + final helper = Mock(FileIORaspHelper) + FileIORaspHelper.INSTANCE = helper final file = new URI('file:/test.txt') when: diff --git a/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileLoadedRaspHelperForkedTest.groovy b/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileIORaspHelperForkedTest.groovy similarity index 63% rename from dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileLoadedRaspHelperForkedTest.groovy rename to dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileIORaspHelperForkedTest.groovy index 99b81fffe6c..a2b0f6a93df 100644 --- a/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileLoadedRaspHelperForkedTest.groovy +++ b/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileIORaspHelperForkedTest.groovy @@ -3,13 +3,13 @@ package datadog.trace.instrumentation.java.io import datadog.trace.api.gateway.CallbackProvider import datadog.trace.api.gateway.Flow import datadog.trace.api.gateway.RequestContextSlot -import datadog.trace.instrumentation.java.lang.FileLoadedRaspHelper +import datadog.trace.instrumentation.java.lang.FileIORaspHelper import java.util.function.BiFunction import static datadog.trace.api.gateway.Events.EVENTS -class FileLoadedRaspHelperForkedTest extends BaseIoRaspCallSiteTest { +class FileIORaspHelperForkedTest extends BaseIoRaspCallSiteTest { void 'test Helper'() { setup: @@ -19,7 +19,7 @@ class FileLoadedRaspHelperForkedTest extends BaseIoRaspCallSiteTest { tracer.getCallbackProvider(RequestContextSlot.APPSEC) >> callbackProvider when: - FileLoadedRaspHelper.INSTANCE.beforeFileLoaded(*args) + FileIORaspHelper.INSTANCE.beforeFileLoaded(*args) then: 1 * callbackProvider.getCallback(EVENTS.fileLoaded()) >> listener @@ -34,4 +34,19 @@ class FileLoadedRaspHelperForkedTest extends BaseIoRaspCallSiteTest { ['/tmp', ['log', 'test.txt'] as String[]] | '/tmp/log/test.txt' ['test.txt', [] as String[]] | 'test.txt' } + + void 'test beforeFileWritten'() { + setup: + final callbackProvider = Mock(CallbackProvider) + final listener = Mock(BiFunction) + final flow = Mock(Flow) + tracer.getCallbackProvider(RequestContextSlot.APPSEC) >> callbackProvider + + when: + FileIORaspHelper.INSTANCE.beforeFileWritten('test.txt') + + then: + 1 * callbackProvider.getCallback(EVENTS.fileWritten()) >> listener + 1 * listener.apply(reqCtx, 'test.txt') >> flow + } } diff --git a/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileInputStreamCallSiteTest.groovy b/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileInputStreamCallSiteTest.groovy index b1d97031e75..5aca165af9f 100644 --- a/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileInputStreamCallSiteTest.groovy +++ b/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileInputStreamCallSiteTest.groovy @@ -2,7 +2,7 @@ package datadog.trace.instrumentation.java.io import datadog.trace.api.iast.InstrumentationBridge import datadog.trace.api.iast.sink.PathTraversalModule -import datadog.trace.instrumentation.java.lang.FileLoadedRaspHelper +import datadog.trace.instrumentation.java.lang.FileIORaspHelper import foo.bar.TestFileInputStreamSuite class FileInputStreamCallSiteTest extends BaseIoRaspCallSiteTest { @@ -22,8 +22,8 @@ class FileInputStreamCallSiteTest extends BaseIoRaspCallSiteTest { void 'test RASP new file input stream with path'() { setup: - final helper = Mock(FileLoadedRaspHelper) - FileLoadedRaspHelper.INSTANCE = helper + final helper = Mock(FileIORaspHelper) + FileIORaspHelper.INSTANCE = helper final path = newFile('test_rasp.txt').toString() when: diff --git a/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileOutputStreamCallSiteTest.groovy b/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileOutputStreamCallSiteTest.groovy index 18ad631bca0..e59e48c9193 100644 --- a/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileOutputStreamCallSiteTest.groovy +++ b/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/FileOutputStreamCallSiteTest.groovy @@ -2,7 +2,7 @@ package datadog.trace.instrumentation.java.io import datadog.trace.api.iast.InstrumentationBridge import datadog.trace.api.iast.sink.PathTraversalModule -import datadog.trace.instrumentation.java.lang.FileLoadedRaspHelper +import datadog.trace.instrumentation.java.lang.FileIORaspHelper import foo.bar.TestFileOutputStreamSuite import groovy.transform.CompileDynamic @@ -37,27 +37,27 @@ class FileOutputStreamCallSiteTest extends BaseIoRaspCallSiteTest { void 'test RASP new file input stream with path'() { setup: - final helper = Mock(FileLoadedRaspHelper) - FileLoadedRaspHelper.INSTANCE = helper + final helper = Mock(FileIORaspHelper) + FileIORaspHelper.INSTANCE = helper final path = newFile('test_rasp_1.txt').toString() when: TestFileOutputStreamSuite.newFileOutputStream(path) then: - 1 * helper.beforeFileLoaded(path) + 1 * helper.beforeFileWritten(path) } void 'test RASP new file input stream with path and append'() { setup: - final helper = Mock(FileLoadedRaspHelper) - FileLoadedRaspHelper.INSTANCE = helper + final helper = Mock(FileIORaspHelper) + FileIORaspHelper.INSTANCE = helper final path = newFile('test_rasp_2.txt').toString() when: TestFileOutputStreamSuite.newFileOutputStream(path, false) then: - 1 * helper.beforeFileLoaded(path) + 1 * helper.beforeFileWritten(path) } } diff --git a/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/PathCallSiteTest.groovy b/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/PathCallSiteTest.groovy index d2d4cb4c056..0527a0571fa 100644 --- a/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/PathCallSiteTest.groovy +++ b/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/PathCallSiteTest.groovy @@ -2,7 +2,7 @@ package datadog.trace.instrumentation.java.io import datadog.trace.api.iast.InstrumentationBridge import datadog.trace.api.iast.sink.PathTraversalModule -import datadog.trace.instrumentation.java.lang.FileLoadedRaspHelper +import datadog.trace.instrumentation.java.lang.FileIORaspHelper import foo.bar.TestPathSuite class PathCallSiteTest extends BaseIoRaspCallSiteTest { @@ -36,8 +36,8 @@ class PathCallSiteTest extends BaseIoRaspCallSiteTest { void 'test RASP resolve path'() { setup: - final helper = Mock(FileLoadedRaspHelper) - FileLoadedRaspHelper.INSTANCE = helper + final helper = Mock(FileIORaspHelper) + FileIORaspHelper.INSTANCE = helper final path = 'test_rasp.txt' when: @@ -49,8 +49,8 @@ class PathCallSiteTest extends BaseIoRaspCallSiteTest { void 'test RASP resolve sibling'() { setup: - final helper = Mock(FileLoadedRaspHelper) - FileLoadedRaspHelper.INSTANCE = helper + final helper = Mock(FileIORaspHelper) + FileIORaspHelper.INSTANCE = helper final sibling = newFile('test_rasp_1.txt').toPath() final path = 'test_rasp_2.txt' diff --git a/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/PathsCallSiteTest.groovy b/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/PathsCallSiteTest.groovy index 2b06300a3ed..786b84e51df 100644 --- a/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/PathsCallSiteTest.groovy +++ b/dd-java-agent/instrumentation/java/java-io-1.8/src/test/groovy/datadog/trace/instrumentation/java/io/PathsCallSiteTest.groovy @@ -2,7 +2,7 @@ package datadog.trace.instrumentation.java.io import datadog.trace.api.iast.InstrumentationBridge import datadog.trace.api.iast.sink.PathTraversalModule -import datadog.trace.instrumentation.java.lang.FileLoadedRaspHelper +import datadog.trace.instrumentation.java.lang.FileIORaspHelper import foo.bar.TestPathsSuite class PathsCallSiteTest extends BaseIoRaspCallSiteTest { @@ -39,8 +39,8 @@ class PathsCallSiteTest extends BaseIoRaspCallSiteTest { void 'test RASP get path from strings'(final String first, final String... other) { setup: - final helper = Mock(FileLoadedRaspHelper) - FileLoadedRaspHelper.INSTANCE = helper + final helper = Mock(FileIORaspHelper) + FileIORaspHelper.INSTANCE = helper when: TestPathsSuite.get(first, other) @@ -56,8 +56,8 @@ class PathsCallSiteTest extends BaseIoRaspCallSiteTest { void 'test RASP get path from uri'() { setup: - final helper = Mock(FileLoadedRaspHelper) - FileLoadedRaspHelper.INSTANCE = helper + final helper = Mock(FileIORaspHelper) + FileIORaspHelper.INSTANCE = helper final file = new URI('file:/test.txt') when: diff --git a/dd-smoke-tests/appsec/springboot/src/main/java/datadog/smoketest/appsec/springboot/controller/WebController.java b/dd-smoke-tests/appsec/springboot/src/main/java/datadog/smoketest/appsec/springboot/controller/WebController.java index 55efc9de0b8..e9237b0cd6b 100644 --- a/dd-smoke-tests/appsec/springboot/src/main/java/datadog/smoketest/appsec/springboot/controller/WebController.java +++ b/dd-smoke-tests/appsec/springboot/src/main/java/datadog/smoketest/appsec/springboot/controller/WebController.java @@ -12,6 +12,7 @@ import datadog.smoketest.appsec.springboot.service.AsyncService; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -190,6 +191,12 @@ public String lfiPath(@RequestParam("path") String path) { return "EXECUTED"; } + @GetMapping("/lfi/fileoutputstream") + public String lfiFileOutputStream(@RequestParam("path") String path) throws IOException { + new FileOutputStream(path).close(); + return "EXECUTED"; + } + @RequestMapping("/session") public ResponseEntity session(final HttpServletRequest request) { final HttpSession session = request.getSession(true); diff --git a/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/SpringBootSmokeTest.groovy b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/SpringBootSmokeTest.groovy index 80c4bb4949c..e413cae5b77 100644 --- a/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/SpringBootSmokeTest.groovy +++ b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/SpringBootSmokeTest.groovy @@ -766,6 +766,39 @@ class SpringBootSmokeTest extends AbstractAppSecServerSmokeTest { 'path' | _ } + void 'rasp blocks on LFI write'() { + when: + String url = "http://localhost:${httpPort}/lfi/fileoutputstream?path=." + URLEncoder.encode("../../../etc/passwd", StandardCharsets.UTF_8.name()) + def request = new Request.Builder() + .url(url) + .get() + .build() + def response = client.newCall(request).execute() + def responseBodyStr = response.body().string() + + then: + response.code() == 403 + responseBodyStr.contains('You\'ve been blocked') + + when: + waitForTraceCount(1) + + then: + def rootSpan = findFirstMatchingSpan('fileoutputstream') + assert rootSpan != null, 'root span not found' + assert rootSpan.meta.get('appsec.blocked') == 'true', 'appsec.blocked is not set' + assert rootSpan.meta.get('_dd.appsec.json') != null, '_dd.appsec.json is not set' + def trigger = null + for (t in rootSpan.triggers) { + if (t['rule']['id'] == 'rasp-930-100') { + trigger = t + break + } + } + assert trigger != null, 'test trigger not found' + rootSpan.span.metaStruct == null + } + def findFirstMatchingSpan(String resource) { return this.rootSpans.toList().find { (it.span.resource == 'GET /lfi/' + resource) } } diff --git a/internal-api/src/main/java/datadog/trace/api/gateway/Events.java b/internal-api/src/main/java/datadog/trace/api/gateway/Events.java index 0a751b637f0..d51261b1196 100644 --- a/internal-api/src/main/java/datadog/trace/api/gateway/Events.java +++ b/internal-api/src/main/java/datadog/trace/api/gateway/Events.java @@ -396,6 +396,17 @@ public EventType, Flow>> requestFi REQUEST_FILES_FILENAMES; } + static final int FILE_WRITTEN_ID = 31; + + @SuppressWarnings("rawtypes") + private static final EventType FILE_WRITTEN = new ET<>("file.written", FILE_WRITTEN_ID); + + /** An I/O file written */ + @SuppressWarnings("unchecked") + public EventType>> fileWritten() { + return (EventType>>) FILE_WRITTEN; + } + static final int MAX_EVENTS = nextId.get(); private static final class ET extends EventType { diff --git a/internal-api/src/main/java/datadog/trace/api/gateway/InstrumentationGateway.java b/internal-api/src/main/java/datadog/trace/api/gateway/InstrumentationGateway.java index 553732422a6..0e8a4174117 100644 --- a/internal-api/src/main/java/datadog/trace/api/gateway/InstrumentationGateway.java +++ b/internal-api/src/main/java/datadog/trace/api/gateway/InstrumentationGateway.java @@ -4,6 +4,7 @@ import static datadog.trace.api.gateway.Events.DATABASE_SQL_QUERY_ID; import static datadog.trace.api.gateway.Events.EXEC_CMD_ID; import static datadog.trace.api.gateway.Events.FILE_LOADED_ID; +import static datadog.trace.api.gateway.Events.FILE_WRITTEN_ID; import static datadog.trace.api.gateway.Events.GRAPHQL_SERVER_REQUEST_MESSAGE_ID; import static datadog.trace.api.gateway.Events.GRPC_SERVER_METHOD_ID; import static datadog.trace.api.gateway.Events.GRPC_SERVER_REQUEST_MESSAGE_ID; @@ -447,6 +448,7 @@ public Flow apply(RequestContext ctx, String arg) { }; case DATABASE_SQL_QUERY_ID: case FILE_LOADED_ID: + case FILE_WRITTEN_ID: case SHELL_CMD_ID: return (C) new BiFunction>() { diff --git a/internal-api/src/test/java/datadog/trace/api/gateway/InstrumentationGatewayTest.java b/internal-api/src/test/java/datadog/trace/api/gateway/InstrumentationGatewayTest.java index 47864e05063..c23331f7341 100644 --- a/internal-api/src/test/java/datadog/trace/api/gateway/InstrumentationGatewayTest.java +++ b/internal-api/src/test/java/datadog/trace/api/gateway/InstrumentationGatewayTest.java @@ -240,6 +240,8 @@ public void testNormalCalls() { assertEquals( Flow.Action.Noop.INSTANCE, cbp.getCallback(events.requestFilesFilenames()).apply(null, null).getAction()); + ss.registerCallback(events.fileWritten(), callback); + cbp.getCallback(events.fileWritten()).apply(null, null); assertEquals(Events.MAX_EVENTS, callback.count); } @@ -329,6 +331,8 @@ public void testThrowableBlocking() { ss.registerCallback(events.requestFilesFilenames(), throwback); assertEquals( Flow.ResultFlow.empty(), cbp.getCallback(events.requestFilesFilenames()).apply(null, null)); + ss.registerCallback(events.fileWritten(), throwback); + cbp.getCallback(events.fileWritten()).apply(null, null); assertEquals(Events.MAX_EVENTS, throwback.count); }