From 809a17bf86675a43aace79d288ea3bf581310576 Mon Sep 17 00:00:00 2001 From: Christophe Papazian Date: Wed, 27 May 2026 16:45:15 +0200 Subject: [PATCH 1/6] feat(appsec): collect security-testing headers on HTTP entry spans (APPSEC-62412) Unconditionally collect x-datadog-endpoint-scan and x-datadog-security-test HTTP request headers as http.request.headers.* tags on the service entry span, independent of DD_TRACE_HEADER_TAGS and AppSec enablement. Also forwarded to the inferred proxy span. Covers all PHP SAPIs (Apache/fpm, CLI, FrankenPHP, RoadRunner) via the C extension and Swoole via its PHP integration. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 1 + ext/serializer.c | 26 +++++++ .../Integrations/Swoole/SwooleIntegration.php | 8 +++ .../Swoole/SecurityTestingHeadersTest.php | 71 +++++++++++++++++++ .../security_headers_forwarded.phpt | 52 ++++++++++++++ .../root_span_security_testing_headers.phpt | 21 ++++++ ..._span_security_testing_headers_absent.phpt | 18 +++++ 7 files changed, 197 insertions(+) create mode 100644 tests/Integrations/Swoole/SecurityTestingHeadersTest.php create mode 100644 tests/ext/inferred_proxy/security_headers_forwarded.phpt create mode 100644 tests/ext/root_span_security_testing_headers.phpt create mode 100644 tests/ext/root_span_security_testing_headers_absent.phpt diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bd206c9973..119720aba5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Changelog for older versions can be found in our [release page](https://github.c ## Tracer ### Added +- Unconditionally collect `x-datadog-endpoint-scan` and `x-datadog-security-test` HTTP request headers as span tags on HTTP entry spans, independent of `DD_TRACE_HEADER_TAGS` and AppSec enablement (APPSEC-62412) - Add support for OpenTelemetry logs (`DD_LOGS_OTEL_ENABLED=true`, disabled by default) #3748 ### Changed diff --git a/ext/serializer.c b/ext/serializer.c index b9ec42a9516..3e37838f52b 100644 --- a/ext/serializer.c +++ b/ext/serializer.c @@ -693,6 +693,30 @@ static void dd_set_entrypoint_root_span_props(struct superglob_equiv *data, ddtr } if (data->server) { + // Unconditionally collect security-testing headers (APPSEC-62412) + static const struct { + const char *server_key; size_t server_len; + const char *tag; size_t tag_len; + } sec_headers[] = { + { "HTTP_X_DATADOG_ENDPOINT_SCAN", sizeof("HTTP_X_DATADOG_ENDPOINT_SCAN") - 1, + "http.request.headers.x-datadog-endpoint-scan", + sizeof("http.request.headers.x-datadog-endpoint-scan") - 1 }, + { "HTTP_X_DATADOG_SECURITY_TEST", sizeof("HTTP_X_DATADOG_SECURITY_TEST") - 1, + "http.request.headers.x-datadog-security-test", + sizeof("http.request.headers.x-datadog-security-test") - 1 }, + }; + 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 +1872,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, "http.request.headers.x-datadog-endpoint-scan", false); + transfer_meta_data(rust_span, serialized_inferred_span, "http.request.headers.x-datadog-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..793d5987598 --- /dev/null +++ b/tests/ext/inferred_proxy/security_headers_forwarded.phpt @@ -0,0 +1,52 @@ +--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 +--GET-- +foo=bar +--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..6cec523ba4d --- /dev/null +++ b/tests/ext/root_span_security_testing_headers.phpt @@ -0,0 +1,21 @@ +--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 +--GET-- +foo=bar +--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..b015893f0bc --- /dev/null +++ b/tests/ext/root_span_security_testing_headers_absent.phpt @@ -0,0 +1,18 @@ +--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 +--GET-- +foo=bar +--FILE-- + +--EXPECT-- +bool(false) +bool(false) From 7e736fa4ad2ec15f51e035187ab340a956220322 Mon Sep 17 00:00:00 2001 From: Christophe Papazian Date: Thu, 28 May 2026 16:21:58 +0200 Subject: [PATCH 2/6] Address review: move header collection to tags.c, drop CHANGELOG - Move x-datadog-endpoint-scan and x-datadog-security-test collection to appsec/src/extension/tags.c via _relevant_basic_headers, following the existing pattern for http.request.headers.* tags - Remove duplicate logic from ext/serializer.c (keep transfer_meta_data calls for inferred proxy span forwarding) - Replace tracer phpt tests with an AppSec extension test using add_all/add_basic_ancillary_tags - Remove CHANGELOG entry (added at release time) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 1 - appsec/src/extension/tags.c | 2 + .../extension/security_testing_headers.phpt | 29 +++++++++++ ext/serializer.c | 24 --------- .../security_headers_forwarded.phpt | 52 ------------------- .../root_span_security_testing_headers.phpt | 21 -------- ..._span_security_testing_headers_absent.phpt | 18 ------- 7 files changed, 31 insertions(+), 116 deletions(-) create mode 100644 appsec/tests/extension/security_testing_headers.phpt delete mode 100644 tests/ext/inferred_proxy/security_headers_forwarded.phpt delete mode 100644 tests/ext/root_span_security_testing_headers.phpt delete mode 100644 tests/ext/root_span_security_testing_headers_absent.phpt diff --git a/CHANGELOG.md b/CHANGELOG.md index 119720aba5c..5bd206c9973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ Changelog for older versions can be found in our [release page](https://github.c ## Tracer ### Added -- Unconditionally collect `x-datadog-endpoint-scan` and `x-datadog-security-test` HTTP request headers as span tags on HTTP entry spans, independent of `DD_TRACE_HEADER_TAGS` and AppSec enablement (APPSEC-62412) - Add support for OpenTelemetry logs (`DD_LOGS_OTEL_ENABLED=true`, disabled by default) #3748 ### Changed diff --git a/appsec/src/extension/tags.c b/appsec/src/extension/tags.c index ed243e5a859..94cd556e9ef 100644 --- a/appsec/src/extension/tags.c +++ b/appsec/src/extension/tags.c @@ -305,6 +305,8 @@ static void _init_relevant_headers(void) ADD_RELEVANT_BASIC_HEADER("content-type"); ADD_RELEVANT_BASIC_HEADER("user-agent"); ADD_RELEVANT_BASIC_HEADER("accept"); + ADD_RELEVANT_BASIC_HEADER("x-datadog-endpoint-scan"); // APPSEC-62412 + ADD_RELEVANT_BASIC_HEADER("x-datadog-security-test"); // APPSEC-62412 ADD_RELEVANT_HEADER("x-forwarded-for"); ADD_RELEVANT_HEADER("x-client-ip"); diff --git a/appsec/tests/extension/security_testing_headers.phpt b/appsec/tests/extension/security_testing_headers.phpt new file mode 100644 index 00000000000..7ac4acf99c6 --- /dev/null +++ b/appsec/tests/extension/security_testing_headers.phpt @@ -0,0 +1,29 @@ +--TEST-- +Security-testing headers are collected on the root span +--INI-- +extension=ddtrace.so +--ENV-- +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" +string(18) "endpoint-scan-uuid" +string(18) "security-test-uuid" diff --git a/ext/serializer.c b/ext/serializer.c index 3e37838f52b..1a194cc4c6b 100644 --- a/ext/serializer.c +++ b/ext/serializer.c @@ -693,30 +693,6 @@ static void dd_set_entrypoint_root_span_props(struct superglob_equiv *data, ddtr } if (data->server) { - // Unconditionally collect security-testing headers (APPSEC-62412) - static const struct { - const char *server_key; size_t server_len; - const char *tag; size_t tag_len; - } sec_headers[] = { - { "HTTP_X_DATADOG_ENDPOINT_SCAN", sizeof("HTTP_X_DATADOG_ENDPOINT_SCAN") - 1, - "http.request.headers.x-datadog-endpoint-scan", - sizeof("http.request.headers.x-datadog-endpoint-scan") - 1 }, - { "HTTP_X_DATADOG_SECURITY_TEST", sizeof("HTTP_X_DATADOG_SECURITY_TEST") - 1, - "http.request.headers.x-datadog-security-test", - sizeof("http.request.headers.x-datadog-security-test") - 1 }, - }; - 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) { diff --git a/tests/ext/inferred_proxy/security_headers_forwarded.phpt b/tests/ext/inferred_proxy/security_headers_forwarded.phpt deleted file mode 100644 index 793d5987598..00000000000 --- a/tests/ext/inferred_proxy/security_headers_forwarded.phpt +++ /dev/null @@ -1,52 +0,0 @@ ---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 ---GET-- -foo=bar ---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 deleted file mode 100644 index 6cec523ba4d..00000000000 --- a/tests/ext/root_span_security_testing_headers.phpt +++ /dev/null @@ -1,21 +0,0 @@ ---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 ---GET-- -foo=bar ---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 deleted file mode 100644 index b015893f0bc..00000000000 --- a/tests/ext/root_span_security_testing_headers_absent.phpt +++ /dev/null @@ -1,18 +0,0 @@ ---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 ---GET-- -foo=bar ---FILE-- - ---EXPECT-- -bool(false) -bool(false) From abd3a5329ce86e5f943e85af8605b0d55e9c37bc Mon Sep 17 00:00:00 2001 From: Christophe Papazian Date: Thu, 28 May 2026 18:00:38 +0200 Subject: [PATCH 3/6] Address review comments on security-testing headers - Add DD_TAG_HTTP_REQH_ENDPOINT_SCAN / DD_TAG_HTTP_REQH_SECURITY_TEST constants so tag-name strings are not duplicated across the sec_headers table and the transfer_meta_data calls - Add DD_UNCONDITIONAL_SERVER_HEADER macro to reduce verbosity of the struct initializer, following reviewer suggestion - Restore serializer.c collection (unconditional, per RFC) alongside tags.c which covers the AppSec-loaded path - Remove CHANGELOG entry (added at release time per reviewer) - Remove --GET-- from phpt tests (unnecessary) - Add AppSec extension test using add_all/add_basic_ancillary_tags - Update ancillary_tags.phpt canonical test to include the two new headers Co-Authored-By: Claude Sonnet 4.6 (1M context) --- appsec/src/extension/tags.c | 4 +- appsec/tests/extension/ancillary_tags.phpt | 6 +++ ext/serializer.c | 33 +++++++++++- .../security_headers_forwarded.phpt | 50 +++++++++++++++++++ .../root_span_security_testing_headers.phpt | 19 +++++++ ..._span_security_testing_headers_absent.phpt | 16 ++++++ 6 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 tests/ext/inferred_proxy/security_headers_forwarded.phpt create mode 100644 tests/ext/root_span_security_testing_headers.phpt create mode 100644 tests/ext/root_span_security_testing_headers_absent.phpt diff --git a/appsec/src/extension/tags.c b/appsec/src/extension/tags.c index 94cd556e9ef..3a08a1d910e 100644 --- a/appsec/src/extension/tags.c +++ b/appsec/src/extension/tags.c @@ -305,8 +305,8 @@ static void _init_relevant_headers(void) ADD_RELEVANT_BASIC_HEADER("content-type"); ADD_RELEVANT_BASIC_HEADER("user-agent"); ADD_RELEVANT_BASIC_HEADER("accept"); - ADD_RELEVANT_BASIC_HEADER("x-datadog-endpoint-scan"); // APPSEC-62412 - ADD_RELEVANT_BASIC_HEADER("x-datadog-security-test"); // APPSEC-62412 + ADD_RELEVANT_BASIC_HEADER("x-datadog-endpoint-scan"); + ADD_RELEVANT_BASIC_HEADER("x-datadog-security-test"); ADD_RELEVANT_HEADER("x-forwarded-for"); ADD_RELEVANT_HEADER("x-client-ip"); diff --git a/appsec/tests/extension/ancillary_tags.phpt b/appsec/tests/extension/ancillary_tags.phpt index d57d5bd8ba9..a09f53ae61c 100644 --- a/appsec/tests/extension/ancillary_tags.phpt +++ b/appsec/tests/extension/ancillary_tags.phpt @@ -38,6 +38,8 @@ HTTP_X_APPGW_TRACE_ID=appgvtraceid HTTP_X_SIGSCI_REQUESTID=sigscirequestid HTTP_X_SIGSCI_TAGS=sigscitags HTTP_AKAMAI_USER_RISK=akamaiuserisk +HTTP_X_DATADOG_ENDPOINT_SCAN=endpoint-scan-uuid +HTTP_X_DATADOG_SECURITY_TEST=security-test-uuid REMOTE_ADDR=7.7.7.12 HTTPS=on --GET-- @@ -98,6 +100,8 @@ Array [http.request.headers.x-client-ip] => 7.7.7.7 [http.request.headers.x-cloud-trace-context] => cloudtracecontext [http.request.headers.x-cluster-client-ip] => 7.7.7.9 + [http.request.headers.x-datadog-endpoint-scan] => endpoint-scan-uuid + [http.request.headers.x-datadog-security-test] => security-test-uuid [http.request.headers.x-forwarded] => for="foo" [http.request.headers.x-forwarded-for] => 7.7.7.6,10.0.0.1 [http.request.headers.x-real-ip] => 7.7.7.8 @@ -124,6 +128,8 @@ Array [http.request.headers.x-amzn-trace-id] => amazontraceid [http.request.headers.x-appgw-trace-id] => appgvtraceid [http.request.headers.x-cloud-trace-context] => cloudtracecontext + [http.request.headers.x-datadog-endpoint-scan] => endpoint-scan-uuid + [http.request.headers.x-datadog-security-test] => security-test-uuid [http.request.headers.x-sigsci-requestid] => sigscirequestid [http.request.headers.x-sigsci-tags] => sigscitags [http.response.headers.content-encoding] => foobar diff --git a/ext/serializer.c b/ext/serializer.c index 1a194cc4c6b..c42ec0b0e1d 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,32 @@ static void dd_set_entrypoint_root_span_props(struct superglob_equiv *data, ddtr } if (data->server) { + // Security-testing headers (APPSEC-62412): collected unconditionally + // here so they are present even when the AppSec extension is not loaded. + // The AppSec extension also collects them via _relevant_basic_headers + // in appsec/src/extension/tags.c. +#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,8 +1877,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, "http.request.headers.x-datadog-endpoint-scan", false); - transfer_meta_data(rust_span, serialized_inferred_span, "http.request.headers.x-datadog-security-test", false); + 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/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) From 434a266a2255b108a88e52b87515c1bdef0aa724 Mon Sep 17 00:00:00 2001 From: Christophe Papazian Date: Mon, 1 Jun 2026 14:11:59 +0200 Subject: [PATCH 4/6] Remove redundant AppSec tags.c collection Since the headers are already collected unconditionally by ext/serializer.c (always loaded), adding them to AppSec's _relevant_basic_headers is unnecessary. Remove both the tags.c addition and its dedicated test. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- appsec/src/extension/tags.c | 2 -- .../extension/security_testing_headers.phpt | 29 ------------------- 2 files changed, 31 deletions(-) delete mode 100644 appsec/tests/extension/security_testing_headers.phpt diff --git a/appsec/src/extension/tags.c b/appsec/src/extension/tags.c index 3a08a1d910e..ed243e5a859 100644 --- a/appsec/src/extension/tags.c +++ b/appsec/src/extension/tags.c @@ -305,8 +305,6 @@ static void _init_relevant_headers(void) ADD_RELEVANT_BASIC_HEADER("content-type"); ADD_RELEVANT_BASIC_HEADER("user-agent"); ADD_RELEVANT_BASIC_HEADER("accept"); - ADD_RELEVANT_BASIC_HEADER("x-datadog-endpoint-scan"); - ADD_RELEVANT_BASIC_HEADER("x-datadog-security-test"); ADD_RELEVANT_HEADER("x-forwarded-for"); ADD_RELEVANT_HEADER("x-client-ip"); diff --git a/appsec/tests/extension/security_testing_headers.phpt b/appsec/tests/extension/security_testing_headers.phpt deleted file mode 100644 index 7ac4acf99c6..00000000000 --- a/appsec/tests/extension/security_testing_headers.phpt +++ /dev/null @@ -1,29 +0,0 @@ ---TEST-- -Security-testing headers are collected on the root span ---INI-- -extension=ddtrace.so ---ENV-- -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" -string(18) "endpoint-scan-uuid" -string(18) "security-test-uuid" From e18e9082d95268d3b5d033f130669abe8c91dfd2 Mon Sep 17 00:00:00 2001 From: Christophe Papazian Date: Mon, 1 Jun 2026 14:29:22 +0200 Subject: [PATCH 5/6] Revert ancillary_tags.phpt to master state The x-datadog headers were removed from tags.c _relevant_basic_headers, so ancillary_tags.phpt no longer expects them in the output. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- appsec/tests/extension/ancillary_tags.phpt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/appsec/tests/extension/ancillary_tags.phpt b/appsec/tests/extension/ancillary_tags.phpt index a09f53ae61c..d57d5bd8ba9 100644 --- a/appsec/tests/extension/ancillary_tags.phpt +++ b/appsec/tests/extension/ancillary_tags.phpt @@ -38,8 +38,6 @@ HTTP_X_APPGW_TRACE_ID=appgvtraceid HTTP_X_SIGSCI_REQUESTID=sigscirequestid HTTP_X_SIGSCI_TAGS=sigscitags HTTP_AKAMAI_USER_RISK=akamaiuserisk -HTTP_X_DATADOG_ENDPOINT_SCAN=endpoint-scan-uuid -HTTP_X_DATADOG_SECURITY_TEST=security-test-uuid REMOTE_ADDR=7.7.7.12 HTTPS=on --GET-- @@ -100,8 +98,6 @@ Array [http.request.headers.x-client-ip] => 7.7.7.7 [http.request.headers.x-cloud-trace-context] => cloudtracecontext [http.request.headers.x-cluster-client-ip] => 7.7.7.9 - [http.request.headers.x-datadog-endpoint-scan] => endpoint-scan-uuid - [http.request.headers.x-datadog-security-test] => security-test-uuid [http.request.headers.x-forwarded] => for="foo" [http.request.headers.x-forwarded-for] => 7.7.7.6,10.0.0.1 [http.request.headers.x-real-ip] => 7.7.7.8 @@ -128,8 +124,6 @@ Array [http.request.headers.x-amzn-trace-id] => amazontraceid [http.request.headers.x-appgw-trace-id] => appgvtraceid [http.request.headers.x-cloud-trace-context] => cloudtracecontext - [http.request.headers.x-datadog-endpoint-scan] => endpoint-scan-uuid - [http.request.headers.x-datadog-security-test] => security-test-uuid [http.request.headers.x-sigsci-requestid] => sigscirequestid [http.request.headers.x-sigsci-tags] => sigscitags [http.response.headers.content-encoding] => foobar From a99f53e887cdf91a4409c5d146bde8e548d0f109 Mon Sep 17 00:00:00 2001 From: Christophe Papazian Date: Mon, 1 Jun 2026 15:20:20 +0200 Subject: [PATCH 6/6] Fix stale comment in serializer.c Remove reference to appsec/src/extension/tags.c that was reverted. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- ext/serializer.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ext/serializer.c b/ext/serializer.c index c42ec0b0e1d..0c5107a2542 100644 --- a/ext/serializer.c +++ b/ext/serializer.c @@ -697,9 +697,7 @@ static void dd_set_entrypoint_root_span_props(struct superglob_equiv *data, ddtr if (data->server) { // Security-testing headers (APPSEC-62412): collected unconditionally - // here so they are present even when the AppSec extension is not loaded. - // The AppSec extension also collects them via _relevant_basic_headers - // in appsec/src/extension/tags.c. + // 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 {