diff --git a/ext/configuration.c b/ext/configuration.c index e205ce8cdf5..2d5a8b7ea7a 100644 --- a/ext/configuration.c +++ b/ext/configuration.c @@ -13,6 +13,8 @@ ZEND_EXTERN_MODULE_GLOBALS(datadog); #include +static bool ddtrace_alter_DD_TRACE_HEADER_TAGS(zval *old_value, zval *new_value, zend_string *new_str); + #define DD_TO_DATADOG_INC 5 /* "DD" expanded to "datadog" */ #define APPLY_0(...) @@ -135,6 +137,28 @@ static void dd_ini_env_to_ini_name(const zai_str env_name, zai_config_name *ini_ ini_name->ptr[ini_name->len] = '\0'; } +// Security-testing headers (APPSEC-62412) are injected permanently into +// DD_TRACE_HEADER_TAGS so they ride the existing header-tags loop in +// dd_add_header_to_meta with zero extra per-request overhead. +static void dd_ensure_security_testing_headers(zend_array *ht) { + zval empty; + ZVAL_EMPTY_STRING(&empty); + zend_hash_str_add(ht, ZEND_STRL("x-datadog-endpoint-scan"), &empty); + zend_hash_str_add(ht, ZEND_STRL("x-datadog-security-test"), &empty); +} + +static bool ddtrace_alter_DD_TRACE_HEADER_TAGS(zval *old_value, zval *new_value, zend_string *new_str) { + UNUSED(old_value); + if (DATADOG_G(remote_config_state) && !DATADOG_G(remote_config_writing)) { + if (!ddog_remote_config_alter_dynamic_config(DATADOG_G(remote_config_state), + DDOG_CHARSLICE_C("datadog.trace.header_tags"), zend_string_copy(new_str))) { + return false; + } + } + dd_ensure_security_testing_headers(Z_ARR_P(new_value)); + return true; +} + bool datadog_config_minit(int module_number) { if (!zai_config_minit(datadog_config_entries, (sizeof datadog_config_entries / sizeof *datadog_config_entries), dd_ini_env_to_ini_name, module_number)) { @@ -147,6 +171,8 @@ bool datadog_config_minit(int module_number) { // This is intentional, so that places wishing to use values pre-RINIT do have to explicitly opt in by using the // arduous way of accessing the decoded_value directly from zai_config_memoized_entries. zai_config_first_time_rinit(false); + dd_ensure_security_testing_headers( + Z_ARR(zai_config_memoized_entries[DATADOG_CONFIG_DD_TRACE_HEADER_TAGS].decoded_value)); datadog_alter_dd_trace_debug(NULL, &zai_config_memoized_entries[DATADOG_CONFIG_DD_TRACE_DEBUG].decoded_value, NULL); datadog_log_ginit(); 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) diff --git a/tracer/configuration_dependencies.h b/tracer/configuration_dependencies.h index 6024036415d..f1975894166 100644 --- a/tracer/configuration_dependencies.h +++ b/tracer/configuration_dependencies.h @@ -120,7 +120,6 @@ static bool dd_parse_tags(zai_str value, zval *decoded_value, bool persistent) { return ddog_remote_config_alter_dynamic_config(DATADOG_G(remote_config_state), DDOG_CHARSLICE_C(config), zend_string_copy(new_str)); \ } -INI_CHANGE_DYNAMIC_CONFIG(DD_TRACE_HEADER_TAGS, "datadog.trace.header_tags") INI_CHANGE_DYNAMIC_CONFIG(DD_TRACE_SAMPLE_RATE, "datadog.trace.sample_rate") INI_CHANGE_DYNAMIC_CONFIG(DD_TRACE_LOGS_ENABLED, "datadog.logs_injection") INI_CHANGE_DYNAMIC_CONFIG(DD_CODE_ORIGIN_FOR_SPANS_ENABLED, "datadog.code_origin_for_spans_enabled") diff --git a/tracer/serializer.c b/tracer/serializer.c index 018d90ff22f..4af757c4631 100644 --- a/tracer/serializer.c +++ b/tracer/serializer.c @@ -55,6 +55,9 @@ ZEND_EXTERN_MODULE_GLOBALS(datadog); +#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); @@ -1834,6 +1837,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)); }