From d86e0abbb712105a0df6acda3f4cf4d555dea949 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Wed, 5 Feb 2025 11:16:42 +0100 Subject: [PATCH] Add multiple rasp endpoint --- docs/weblog/README.md | 11 + manifests/php.yml | 1 + .../rasp/rasp_ruleset_non_blocking.json | 282 ++++++++++++++++++ tests/appsec/rasp/test_lfi.py | 19 ++ tests/appsec/rasp/utils.py | 5 + utils/_context/_scenarios/__init__.py | 6 +- .../build/docker/php/common/rasp/multiple.php | 6 + 7 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 tests/appsec/rasp/rasp_ruleset_non_blocking.json create mode 100644 utils/build/docker/php/common/rasp/multiple.php diff --git a/docs/weblog/README.md b/docs/weblog/README.md index 97a80fe6a20..f7cb3f17be3 100644 --- a/docs/weblog/README.md +++ b/docs/weblog/README.md @@ -768,6 +768,17 @@ Examples: - `GET`: `/rasp/ssrf?user_id="' OR 1 = 1 --"` - `POST`: `{"user_id": "' OR 1 = 1 --"}` +### \[GET,POST\] /rasp/multiple +The idea of this endpoint is to have an endpoint where multiple rasp operation take place. All of them will generate a MATCH on the WAF but none of them will block. The goal of this endpoint is to verify that the `rasp.rule.match` telemetry entry is updated properly. While this seems easy, the WAF requires that data given on `call` is passed as ephemeral and not as persistent. + +In order to make the test easier, the operation used here need to generate LFI matches. The request will have two get parameters(`file1`, `file2`) which will contain a path that needs to be used as the parameters of the choosen lfi function. Then there will be another call to the lfi function with a harcoded parameter `'../etc/passwd'`. This will make `rasp.rule.match` to be equal to 3. A code example look like: + +``` +lfi_operation($request->get('file1')) +lfi_operation($request->get('file2')) +lfi_operation('../etc/passwd') //This one is harcoded +``` + ### GET /dsm/inject This endpoint is used to validate DSM context injection injects the correct encoding to a headers carrier. diff --git a/manifests/php.yml b/manifests/php.yml index 9800e5def28..8ffce7d6dce 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -177,6 +177,7 @@ tests/: Test_Lfi_Rules_Version: v1.6.2 Test_Lfi_StackTrace: v1.6.2 Test_Lfi_Telemetry: missing_feature + Test_Lfi_Telemetry_Multiple_Exploits: v1.7.0 Test_Lfi_UrlQuery: v1.6.2 Test_Lfi_Waf_Version: v1.6.2 test_shi.py: diff --git a/tests/appsec/rasp/rasp_ruleset_non_blocking.json b/tests/appsec/rasp/rasp_ruleset_non_blocking.json new file mode 100644 index 00000000000..29116831777 --- /dev/null +++ b/tests/appsec/rasp/rasp_ruleset_non_blocking.json @@ -0,0 +1,282 @@ +{ + "version": "2.1", + "metadata": { + "rules_version": "1.99.0" + }, + "rules": [ + { + "id": "rasp-930-111", + "name": "Local file inclusion exploit", + "tags": { + "type": "lfi", + "category": "vulnerability_trigger", + "cwe": "22", + "capec": "1000/255/153/126", + "confidence": "0", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + } + ], + "regex": "asdnjakslnbdklasbdkasbdkl" + }, + "operator": "match_regex" + } + ], + "transformers": [], + "on_match": [ + "stack_trace" + ] + }, + + { + "id": "rasp-930-100", + "name": "Local file inclusion exploit", + "tags": { + "type": "lfi", + "category": "vulnerability_trigger", + "cwe": "22", + "capec": "1000/255/153/126", + "confidence": "0", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "resource": [ + { + "address": "server.io.fs.file" + } + ], + "params": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ] + }, + "operator": "lfi_detector" + } + ], + "transformers": [], + "on_match": [ + "stack_trace" + ] + }, + { + "id": "rasp-934-100", + "name": "Server-side request forgery exploit", + "tags": { + "type": "ssrf", + "category": "vulnerability_trigger", + "cwe": "918", + "capec": "1000/225/115/664", + "confidence": "0", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "resource": [ + { + "address": "server.io.net.url" + } + ], + "params": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ] + }, + "operator": "ssrf_detector" + } + ], + "transformers": [], + "on_match": [ + "stack_trace" + ] + }, + { + "id": "rasp-942-100", + "name": "SQL injection exploit", + "tags": { + "type": "sql_injection", + "category": "vulnerability_trigger", + "cwe": "89", + "capec": "1000/152/248/66", + "confidence": "0", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "resource": [ + { + "address": "server.db.statement" + } + ], + "params": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "db_type": [ + { + "address": "server.db.system" + } + ] + }, + "operator": "sqli_detector" + } + ], + "transformers": [], + "on_match": [ + "stack_trace" + ] + }, + { + "id": "rasp-932-100", + "name": "Shell injection exploit", + "enabled": true, + "tags": { + "type": "command_injection", + "category": "vulnerability_trigger", + "cwe": "77", + "capec": "1000/152/248/88", + "confidence": "0", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "resource": [ + { + "address": "server.sys.shell.cmd" + } + ], + "params": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ] + }, + "operator": "shi_detector" + } + ], + "transformers": [], + "on_match": [ + "stack_trace" + ] + }, + { + "id": "rasp-932-110", + "name": "OS command injection exploit", + "enabled": true, + "tags": { + "type": "command_injection", + "category": "vulnerability_trigger", + "cwe": "77", + "capec": "1000/152/248/88", + "confidence": "0", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "resource": [ + { + "address": "server.sys.exec.cmd" + } + ], + "params": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ] + }, + "operator": "cmdi_detector" + } + ], + "transformers": [], + "on_match": [ + "stack_trace" + ] + } + ] +} diff --git a/tests/appsec/rasp/test_lfi.py b/tests/appsec/rasp/test_lfi.py index 3af0533e8e6..10b4420a956 100644 --- a/tests/appsec/rasp/test_lfi.py +++ b/tests/appsec/rasp/test_lfi.py @@ -2,6 +2,7 @@ # This product includes software developed at Datadog (https://www.datadoghq.com/). # Copyright 2021 Datadog, Inc. + from utils import features, weblog, interfaces, scenarios, rfc from utils import remote_config as rc from utils.dd_constants import Capabilities @@ -134,6 +135,24 @@ def test_lfi_span_tags(self): validate_span_tags(self.r, expected_metrics=["_dd.appsec.rasp.duration_ext", "_dd.appsec.rasp.rule.eval"]) +@rfc("https://docs.google.com/document/d/1vmMqpl8STDk7rJnd3YBsa6O9hCls_XHHdsodD61zr_4/edit#heading=h.96mezjnqf46y") +@features.rasp_span_tags +@features.rasp_local_file_inclusion +@scenarios.appsec_runtime_activation +class Test_Lfi_Telemetry_Multiple_Exploits: + """Validate rasp match telemetry metric works""" + + def setup_rasp_match_tag(self): + self.config_state_1 = rc.rc_state.reset().set_config(*RC_CONSTANTS.CONFIG_ENABLED).apply() + self.config_state_1b = rc.rc_state.set_config(*RC_CONSTANTS.RULES_NON_BLOCKING).apply() + self.r = weblog.get("/rasp/multiple", params={"file1": "../etc/passwd", "file2": "../etc/group"}) + + def test_rasp_match_tag(self): + series_eval = find_series(True, "appsec", "rasp.rule.match") + assert series_eval + assert series_eval[0]["points"][0][1] == 3.0 + + @rfc("https://docs.google.com/document/d/1vmMqpl8STDk7rJnd3YBsa6O9hCls_XHHdsodD61zr_4/edit#heading=h.enmf90juqidf") @features.rasp_stack_trace @features.rasp_local_file_inclusion diff --git a/tests/appsec/rasp/utils.py b/tests/appsec/rasp/utils.py index 1bffabb8966..e03f5a16a2f 100644 --- a/tests/appsec/rasp/utils.py +++ b/tests/appsec/rasp/utils.py @@ -151,6 +151,11 @@ class RC_CONSTANTS: _load_file("./tests/appsec/rasp/rasp_ruleset.json"), ) + RULES_NON_BLOCKING = ( + "datadog/2/ASM_DD/rules/config", + _load_file("./tests/appsec/rasp/rasp_ruleset_non_blocking.json"), + ) + class Base_Rules_Version: """Test libddwaf version""" diff --git a/utils/_context/_scenarios/__init__.py b/utils/_context/_scenarios/__init__.py index e1a86243b8a..03f64f62585 100644 --- a/utils/_context/_scenarios/__init__.py +++ b/utils/_context/_scenarios/__init__.py @@ -236,7 +236,11 @@ class _Scenarios: rc_api_enabled=True, appsec_enabled=False, iast_enabled=False, - weblog_env={"DD_APPSEC_WAF_TIMEOUT": "10000000", "DD_APPSEC_TRACE_RATE_LIMIT": "10000"}, # 10 seconds + weblog_env={ + "DD_APPSEC_WAF_TIMEOUT": "10000000", + "DD_APPSEC_TRACE_RATE_LIMIT": "10000", + "DD_APPSEC_RASP_ENABLED": "true", + }, # 10 seconds doc="", scenario_groups=[ScenarioGroup.APPSEC, ScenarioGroup.APPSEC_RASP], ) diff --git a/utils/build/docker/php/common/rasp/multiple.php b/utils/build/docker/php/common/rasp/multiple.php new file mode 100644 index 00000000000..a258366cee0 --- /dev/null +++ b/utils/build/docker/php/common/rasp/multiple.php @@ -0,0 +1,6 @@ +