diff --git a/appsec/src/extension/backtrace.c b/appsec/src/extension/backtrace.c index c35b69f96e0..af058bbc614 100644 --- a/appsec/src/extension/backtrace.c +++ b/appsec/src/extension/backtrace.c @@ -44,14 +44,16 @@ php_backtrace_frame_to_datadog_backtrace_frame( // NOLINTNEXTLINE(bugprone-easil zval *class = zend_hash_find(frame, _class_field); zval id; ZVAL_LONG(&id, index); -#ifdef TESTING if (file) { // In order to be able to test full path encoded everywhere lets set // only the file name without path + SEPARATE_STRING(file); + zend_string *old_file = Z_STR_P(file); char *file_name = strrchr(Z_STRVAL_P(file), '/'); - Z_TRY_DELREF_P(file); ZVAL_STRINGL(file, file_name + 1, strlen(file_name) - 1); + zend_string_release(old_file); } +#ifdef TESTING #endif if (!function) { diff --git a/appsec/tests/integration/build.gradle b/appsec/tests/integration/build.gradle index fa20894b41f..3855d21ab12 100644 --- a/appsec/tests/integration/build.gradle +++ b/appsec/tests/integration/build.gradle @@ -30,8 +30,9 @@ dependencies { implementation platform('org.testcontainers:testcontainers-bom:1.19.8') implementation "org.testcontainers:junit-jupiter" - implementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2' implementation 'com.flipkart.zjsonpatch:zjsonpatch:0.4.16' + implementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2' + implementation 'org.junit.jupiter:junit-jupiter-params:5.9.2' } test { diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/CommonTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/CommonTests.groovy index 2f0a8150462..7b3064dcc56 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/CommonTests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/CommonTests.groovy @@ -5,10 +5,14 @@ import com.datadog.appsec.php.mock_agent.MsgpackHelper import com.datadog.appsec.php.model.Span import com.datadog.appsec.php.model.Trace import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.containers.Container import java.net.http.HttpRequest import java.net.http.HttpResponse +import java.util.stream.Stream; import org.msgpack.core.MessageUnpacker import org.msgpack.core.MessagePack @@ -208,6 +212,58 @@ trait CommonTests { assert exploit.frames[2].line == 15 } + static Stream getTestData() { + return Arrays.stream(new Arguments[]{ + Arguments.of("file_put_contents", "/tmp/dummy", 9), + Arguments.of("readfile", "/tmp/dummy", 15), + Arguments.of("file_get_contents", "/tmp/dummy", 15), + Arguments.of("fopen", "/tmp/dummy", 12), + Arguments.of("stat", "/tmp/dummy", 15), + Arguments.of("lstat", "/tmp/dummy", 15), + }); + } + + @ParameterizedTest + @MethodSource("getTestData") + void 'file_put_contents generates LFI signal'(String target_function, String path, Integer line) { + HttpRequest req = container.buildReq('/filesystem.php?function='+target_function+"&path="+path).GET().build() + def trace = container.traceFromRequest(req, ofString()) { HttpResponse re -> + assert re.statusCode() == 200 + assert re.body().contains('OK') + } + + Span span = trace.first() + + assert span.metrics."_dd.appsec.enabled" == 1.0d + assert span.metrics."_dd.appsec.waf.duration" > 0.0d + assert span.meta."_dd.appsec.event_rules.version" != '' + + InputStream stream = new ByteArrayInputStream( span.meta_struct."_dd.stack".decodeBase64() ) + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(stream) + List stacks = [] + stacks << MsgpackHelper.unpackSingle(unpacker) + Object exploit = stacks.first().exploit.first() + + assert exploit.language == "php" + assert exploit.id ==~ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ + assert exploit.frames[0].file == "filesystem.php" + assert exploit.frames[0].function == target_function + assert exploit.frames[0].id == 1 + assert exploit.frames[0].line == line + assert exploit.frames[1].file == "filesystem.php" + assert exploit.frames[1].function == "one" + assert exploit.frames[1].id == 2 + assert exploit.frames[1].line == 21 + assert exploit.frames[2].file == "filesystem.php" + assert exploit.frames[2].function == "two" + assert exploit.frames[2].id == 3 + assert exploit.frames[2].line == 25 + assert exploit.frames[3].file == "filesystem.php" + assert exploit.frames[3].function == "three" + assert exploit.frames[3].id == 4 + assert exploit.frames[3].line == 28 + } + @Test void 'user blocking'() { def trace = container.traceFromRequest('/user_id.php?id=user2020') { HttpResponse resp -> diff --git a/appsec/tests/integration/src/test/waf/recommended.json b/appsec/tests/integration/src/test/waf/recommended.json index f06f7f29fcf..73f7e355670 100644 --- a/appsec/tests/integration/src/test/waf/recommended.json +++ b/appsec/tests/integration/src/test/waf/recommended.json @@ -55,6 +55,36 @@ "block" ] }, + { + "id": "rasp-001-001", + "name": "Path traversal attack", + "tags": { + "type": "lfi", + "category": "vulnerability_trigger", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "params": [ + { + "address": "server.request.query" + } + ], + "resource": [ + { + "address": "server.io.fs.file" + } + ] + }, + "operator": "lfi_detector" + } + ], + "transformers": [], + "on_match": [ + "stack_trace" + ] + }, { "id": "blk-001-003", "name": "Block User Addresses", diff --git a/appsec/tests/integration/src/test/www/base/public/filesystem.php b/appsec/tests/integration/src/test/www/base/public/filesystem.php new file mode 100644 index 00000000000..32b8a139380 --- /dev/null +++ b/appsec/tests/integration/src/test/www/base/public/filesystem.php @@ -0,0 +1,31 @@ +