diff --git a/ext/serializer.c b/ext/serializer.c index b9ec42a9516..0c5107a2542 100644 --- a/ext/serializer.c +++ b/ext/serializer.c @@ -56,6 +56,9 @@ ZEND_EXTERN_MODULE_GLOBALS(ddtrace); +#define DD_TAG_HTTP_REQH_ENDPOINT_SCAN "http.request.headers.x-datadog-endpoint-scan" +#define DD_TAG_HTTP_REQH_SECURITY_TEST "http.request.headers.x-datadog-security-test" + extern void (*profiling_notify_trace_finished)(uint64_t local_root_span_id, zai_str span_type, zai_str resource); @@ -693,6 +696,30 @@ static void dd_set_entrypoint_root_span_props(struct superglob_equiv *data, ddtr } if (data->server) { + // Security-testing headers (APPSEC-62412): collected unconditionally + // regardless of DD_TRACE_HEADER_TAGS or AppSec being enabled. +#define DD_UNCONDITIONAL_SERVER_HEADER(server_key, tag) \ + { server_key, sizeof(server_key) - 1, tag, sizeof(tag) - 1 } + static const struct { + const char *server_key; size_t server_len; + const char *tag; size_t tag_len; + } sec_headers[] = { + DD_UNCONDITIONAL_SERVER_HEADER("HTTP_X_DATADOG_ENDPOINT_SCAN", DD_TAG_HTTP_REQH_ENDPOINT_SCAN), + DD_UNCONDITIONAL_SERVER_HEADER("HTTP_X_DATADOG_SECURITY_TEST", DD_TAG_HTTP_REQH_SECURITY_TEST), + }; +#undef DD_UNCONDITIONAL_SERVER_HEADER + for (size_t i = 0; i < sizeof(sec_headers) / sizeof(*sec_headers); i++) { + zval *hval = zend_hash_str_find(data->server, sec_headers[i].server_key, sec_headers[i].server_len); + if (hval) { + ZVAL_DEREF(hval); + if (Z_TYPE_P(hval) == IS_STRING) { + zval zv; + ZVAL_STR_COPY(&zv, Z_STR_P(hval)); + zend_hash_str_add_new(meta, sec_headers[i].tag, sec_headers[i].tag_len, &zv); + } + } + } + zend_string *headername; zval *headerval; ZEND_HASH_FOREACH_STR_KEY_VAL_IND(data->server, headername, headerval) { @@ -1848,6 +1875,8 @@ ddog_SpanBytes *ddtrace_serialize_span_to_rust_span(ddtrace_span_data *span, ddo transfer_meta_data(rust_span, serialized_inferred_span, "_dd.p.dm", true); transfer_meta_data(rust_span, serialized_inferred_span, "_dd.p.ksr", false); transfer_meta_data(rust_span, serialized_inferred_span, "_dd.p.tid", true); + transfer_meta_data(rust_span, serialized_inferred_span, DD_TAG_HTTP_REQH_ENDPOINT_SCAN, false); + transfer_meta_data(rust_span, serialized_inferred_span, DD_TAG_HTTP_REQH_SECURITY_TEST, false); ddog_set_span_error(serialized_inferred_span, ddog_get_span_error(rust_span)); } diff --git a/src/DDTrace/Integrations/Swoole/SwooleIntegration.php b/src/DDTrace/Integrations/Swoole/SwooleIntegration.php index 7d7213795fd..21f3854d8cd 100644 --- a/src/DDTrace/Integrations/Swoole/SwooleIntegration.php +++ b/src/DDTrace/Integrations/Swoole/SwooleIntegration.php @@ -68,6 +68,14 @@ static function (HookData $hook) use ($server, $scheme) { $rootSpan->meta["http.useragent"] = $headers["user-agent"]; } + // Unconditionally collect security-testing headers (APPSEC-62412) + if (isset($headers['x-datadog-endpoint-scan'])) { + $rootSpan->meta['http.request.headers.x-datadog-endpoint-scan'] = $headers['x-datadog-endpoint-scan']; + } + if (isset($headers['x-datadog-security-test'])) { + $rootSpan->meta['http.request.headers.x-datadog-security-test'] = $headers['x-datadog-security-test']; + } + if (!empty(\dd_trace_env_config('DD_TRACE_HTTP_POST_DATA_PARAM_ALLOWED'))) { $rawContent = $request->rawContent(); if ($rawContent) { diff --git a/tests/Integrations/Swoole/SecurityTestingHeadersTest.php b/tests/Integrations/Swoole/SecurityTestingHeadersTest.php new file mode 100644 index 00000000000..6a774f724d7 --- /dev/null +++ b/tests/Integrations/Swoole/SecurityTestingHeadersTest.php @@ -0,0 +1,71 @@ + 'true', + ]); + } + + protected static function getInis() + { + return array_merge(parent::getInis(), [ + 'extension' => 'swoole.so', + ]); + } + + public function testSecurityTestingHeadersCollectedUnconditionally() + { + $traces = $this->tracesFromWebRequest(function () { + $spec = GetSpec::create('request', '/', [ + 'X-Datadog-Endpoint-Scan: endpoint-scan-uuid', + 'X-Datadog-Security-Test: security-test-uuid', + ]); + $this->call($spec); + }); + + $span = $traces[0][0]; + $this->assertSame( + 'endpoint-scan-uuid', + $span['meta']['http.request.headers.x-datadog-endpoint-scan'] + ); + $this->assertSame( + 'security-test-uuid', + $span['meta']['http.request.headers.x-datadog-security-test'] + ); + } + + public function testSecurityTestingHeadersAbsentWhenNotSent() + { + $traces = $this->tracesFromWebRequest(function () { + $this->call(GetSpec::create('request', '/')); + }); + + $span = $traces[0][0]; + $this->assertArrayNotHasKey( + 'http.request.headers.x-datadog-endpoint-scan', + $span['meta'] + ); + $this->assertArrayNotHasKey( + 'http.request.headers.x-datadog-security-test', + $span['meta'] + ); + } +} diff --git a/tests/ext/inferred_proxy/security_headers_forwarded.phpt b/tests/ext/inferred_proxy/security_headers_forwarded.phpt new file mode 100644 index 00000000000..bb960336295 --- /dev/null +++ b/tests/ext/inferred_proxy/security_headers_forwarded.phpt @@ -0,0 +1,50 @@ +--TEST-- +Security-testing headers are forwarded to the inferred proxy span +--ENV-- +DD_TRACE_AUTO_FLUSH_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_CODE_ORIGIN_FOR_SPANS_ENABLED=0 +DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED=1 +HTTP_X_DD_PROXY=aws-apigateway +HTTP_X_DD_PROXY_REQUEST_TIME_MS=100 +HTTP_X_DD_PROXY_PATH=/test +HTTP_X_DD_PROXY_HTTPMETHOD=GET +HTTP_X_DD_PROXY_DOMAIN_NAME=example.com +HTTP_X_DD_PROXY_STAGE=aws-prod +HTTP_X_DATADOG_ENDPOINT_SCAN=endpoint-scan-uuid +HTTP_X_DATADOG_SECURITY_TEST=security-test-uuid +METHOD=GET +SERVER_NAME=localhost:8888 +SCRIPT_NAME=/foo.php +REQUEST_URI=/foo +DD_TRACE_DEBUG_PRNG_SEED=42 +--FILE-- + +--EXPECT-- +string(18) "endpoint-scan-uuid" +string(18) "security-test-uuid" +string(18) "endpoint-scan-uuid" +string(18) "security-test-uuid" diff --git a/tests/ext/root_span_security_testing_headers.phpt b/tests/ext/root_span_security_testing_headers.phpt new file mode 100644 index 00000000000..2f8a9a9cc09 --- /dev/null +++ b/tests/ext/root_span_security_testing_headers.phpt @@ -0,0 +1,19 @@ +--TEST-- +Security-testing headers are collected unconditionally on the root span +--ENV-- +DD_TRACE_AUTO_FLUSH_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_HEADER_TAGS= +HTTP_X_DATADOG_ENDPOINT_SCAN=endpoint-scan-uuid +HTTP_X_DATADOG_SECURITY_TEST=security-test-uuid +--FILE-- + +--EXPECT-- +string(18) "endpoint-scan-uuid" +string(18) "security-test-uuid" diff --git a/tests/ext/root_span_security_testing_headers_absent.phpt b/tests/ext/root_span_security_testing_headers_absent.phpt new file mode 100644 index 00000000000..c80ae66215d --- /dev/null +++ b/tests/ext/root_span_security_testing_headers_absent.phpt @@ -0,0 +1,16 @@ +--TEST-- +Security-testing header tags are absent when headers are not sent +--ENV-- +DD_TRACE_AUTO_FLUSH_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +--FILE-- + +--EXPECT-- +bool(false) +bool(false)