diff --git a/ext/configuration.c b/ext/configuration.c index e205ce8cdf5..e9cadf2a04b 100644 --- a/ext/configuration.c +++ b/ext/configuration.c @@ -142,6 +142,7 @@ bool datadog_config_minit(int module_number) { LOG(ERROR, "Unable to load configuration; likely due to json symbols failing to resolve."); return false; } + // We immediately initialize inis at MINIT, so that we can use a select few values already at minit. // Note that we are not calling zai_config_rinit(), i.e. the get_...() functions will not work. // This is intentional, so that places wishing to use values pre-RINIT do have to explicitly opt in by using the 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/Unit/ConfigurationTest.php b/tests/Unit/ConfigurationTest.php index f707ba4f76c..f3129e5087b 100644 --- a/tests/Unit/ConfigurationTest.php +++ b/tests/Unit/ConfigurationTest.php @@ -280,9 +280,13 @@ public function testGlobalTagsDelimterPrecedence() $this->assertEquals(["env" => "test", "bKey" => "", "dKey" => "", "dVal" => "", "cKey" => ""], \dd_trace_env_config("DD_TAGS")); } - public function testHttpHeadersDefaultsToEmpty() + public function testHttpHeadersDefaultsToSecurityTestingHeaders() { - $this->assertEmpty(\dd_trace_env_config("DD_TRACE_HEADER_TAGS")); + // x-datadog-endpoint-scan and x-datadog-security-test are always present (APPSEC-62412) + $this->assertSame( + ['x-datadog-endpoint-scan' => '', 'x-datadog-security-test' => ''], + \dd_trace_env_config("DD_TRACE_HEADER_TAGS") + ); } public function testHttpHeadersCanSetOne() @@ -290,7 +294,11 @@ public function testHttpHeadersCanSetOne() $this->putEnvAndReloadConfig([ 'DD_TRACE_HEADER_TAGS=A-Header', ]); - $this->assertSame(['a-header'], array_keys(\dd_trace_env_config("DD_TRACE_HEADER_TAGS"))); + // Security-testing headers are always present alongside user-configured ones + $this->assertSame( + ['a-header', 'x-datadog-endpoint-scan', 'x-datadog-security-test'], + array_keys(\dd_trace_env_config("DD_TRACE_HEADER_TAGS")) + ); } public function testHttpHeadersCanSetMultiple() @@ -298,7 +306,13 @@ public function testHttpHeadersCanSetMultiple() $this->putEnvAndReloadConfig([ 'DD_TRACE_HEADER_TAGS=A-Header ,Any-Name , cOn7aining-!spe_cial?:ch/ars , valueless:, Some-Header:with-colon-Key', ]); - $this->assertSame(['a-header', 'any-name', 'con7aining-!spe_cial?', 'valueless', 'some-header'], array_keys(\dd_trace_env_config("DD_TRACE_HEADER_TAGS"))); - $this->assertEquals(['a-header' => '', 'any-name' => '', 'con7aining-!spe_cial?' => 'ch/ars', 'valueless' => '', 'some-header' => 'with-colon-Key'], \dd_trace_env_config("DD_TRACE_HEADER_TAGS")); + $this->assertSame( + ['a-header', 'any-name', 'con7aining-!spe_cial?', 'valueless', 'some-header', 'x-datadog-endpoint-scan', 'x-datadog-security-test'], + array_keys(\dd_trace_env_config("DD_TRACE_HEADER_TAGS")) + ); + $this->assertEquals( + ['a-header' => '', 'any-name' => '', 'con7aining-!spe_cial?' => 'ch/ars', 'valueless' => '', 'some-header' => 'with-colon-Key', 'x-datadog-endpoint-scan' => '', 'x-datadog-security-test' => ''], + \dd_trace_env_config("DD_TRACE_HEADER_TAGS") + ); } } 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.h b/tracer/configuration.h index 4721bfbd6ac..682d1f74fa9 100644 --- a/tracer/configuration.h +++ b/tracer/configuration.h @@ -88,7 +88,7 @@ CONFIG(CUSTOM(INT), DD_TRACE_SAMPLING_RULES_FORMAT, "glob", .parser = dd_parse_sampling_rules_format) \ CONFIG(JSON, DD_SPAN_SAMPLING_RULES, "[]") \ CONFIG(STRING, DD_SPAN_SAMPLING_RULES_FILE, "", .ini_change = ddtrace_alter_sampling_rules_file_config) \ - CONFIG(SET_OR_MAP_LOWERCASE, DD_TRACE_HEADER_TAGS, "", .ini_change = ddtrace_alter_DD_TRACE_HEADER_TAGS) \ + CONFIG(CUSTOM(MAP), DD_TRACE_HEADER_TAGS, "", .ini_change = ddtrace_alter_DD_TRACE_HEADER_TAGS, .parser = dd_parse_header_tags) \ CONFIG(INT, DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH, "512") \ CONFIG(MAP, DD_TRACE_PEER_SERVICE_MAPPING, "") \ CONFIG(BOOL, DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED, "false") \ diff --git a/tracer/configuration_dependencies.h b/tracer/configuration_dependencies.h index 6024036415d..2369c572687 100644 --- a/tracer/configuration_dependencies.h +++ b/tracer/configuration_dependencies.h @@ -110,6 +110,26 @@ static bool dd_parse_tags(zai_str value, zval *decoded_value, bool persistent) { return true; } +// Custom parser to ensure security-testing headers are always captured. +static bool dd_parse_header_tags(zai_str value, zval *decoded_value, bool persistent) { + if (!zai_config_decode_value(value, ZAI_CONFIG_TYPE_SET_OR_MAP_LOWERCASE, NULL, decoded_value, persistent)) { + return false; + } + + HashTable *ht = Z_ARRVAL_P(decoded_value); + zval empty; + if (persistent) { + ZVAL_EMPTY_PSTRING(&empty); + } else { + ZVAL_EMPTY_STRING(&empty); + } + Z_TRY_ADDREF(empty); + zend_hash_str_update(ht, ZEND_STRL("x-datadog-endpoint-scan"), &empty); + zend_hash_str_update(ht, ZEND_STRL("x-datadog-security-test"), &empty); + + return true; +} + #define INI_CHANGE_DYNAMIC_CONFIG(name, config) \ static bool ddtrace_alter_##name(zval *old_value, zval *new_value, zend_string *new_str) { \ UNUSED(old_value, new_value); \ 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)); }