Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions ext/serializer.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would try to reuse as much as possible the code inside dd_add_header_to_meta. Right now it's checking DD_TRACE_HEADER_TAGS which is not needed for this but all other aldd header related code you can reuse it. Also you can add a macro like this to avoid the duplication

#define DD_UNCONDITIONAL_SERVER_HEADER(server_key, tag) \
    { server_key, sizeof(server_key) - 1, tag, sizeof(tag) - 1 }

static const struct { ... } sec_headers[] = {
    DD_UNCONDITIONAL_SERVER_HEADER("HTTP_X_DATADOG_ENDPOINT_SCAN",
                                   "http.request.headers.x-datadog-endpoint-scan"),
    ...
};

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — added the DD_UNCONDITIONAL_SERVER_HEADER macro exactly as suggested, and also moved the tag-name strings to DD_TAG_HTTP_REQH_ENDPOINT_SCAN / DD_TAG_HTTP_REQH_SECURITY_TEST constants (see the follow-up comment). Both are used in the struct initializer and the transfer_meta_data calls.

One note on the tags.c placement: the RFC requires collection to be unconditional regardless of whether AppSec is enabled, so we kept the serializer.c block (tracer, always loaded) and also added the headers to _relevant_basic_headers in tags.c as you suggested — the two paths compose cleanly since zend_hash_str_add_new in the tracer runs at serialization time, and AppSec's _try_add_tag (which uses zend_hash_add) is a no-op if the key already exists.

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);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine this will be sent only in a very small amount of requests. Yet, you're adding overhead in every request looking for them.

Ideally you'd add it to the hastable returned by get_DD_TRACE_HEADER_TAGS() or equivalent, though I think this would require a startup change after zai_config_minit() modyfying zai_config_memoized_entries[DDTRACE_CONFIG_DD_TRACE_HEADER_TAGS].decoded_value and another change in ddtrace_alter_DD_TRACE_HEADER_TAGS, so it might no be worth the complexity.

Anyway, give it a thought.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Two zend_hash_str_find calls per request is O(1) each and minimal in practice, but I understand the concern.

The DD_TRACE_HEADER_TAGS approach would mean piggybacking on the existing HTTP_* loop that already iterates all $_SERVER keys — so overhead-wise it'd be zero extra cost for the absent case, but the loop already runs for every HTTP request anyway. The downside is that it conflates user-configurable header tags with RFC-mandated ones, and as you note it requires non-trivial changes to startup and ddtrace_alter_DD_TRACE_HEADER_TAGS.

I'll leave the current approach as-is given the tradeoff, but happy to revisit if you feel strongly about it.

}

zend_string *headername;
zval *headerval;
ZEND_HASH_FOREACH_STR_KEY_VAL_IND(data->server, headername, headerval) {
Expand Down Expand Up @@ -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);
Comment thread
christophe-papazian marked this conversation as resolved.

ddog_set_span_error(serialized_inferred_span, ddog_get_span_error(rust_span));
}
Expand Down
8 changes: 8 additions & 0 deletions src/DDTrace/Integrations/Swoole/SwooleIntegration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
71 changes: 71 additions & 0 deletions tests/Integrations/Swoole/SecurityTestingHeadersTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace DDTrace\Tests\Integrations\Swoole;

use DDTrace\Tests\Common\WebFrameworkTestCase;
use DDTrace\Tests\Frameworks\Util\Request\GetSpec;

class SecurityTestingHeadersTest extends WebFrameworkTestCase
{
public static function getAppIndexScript()
{
return __DIR__ . '/../../Frameworks/Swoole/index.php';
}

protected static function isSwoole()
{
return true;
}

protected static function getEnvs()
{
return array_merge(parent::getEnvs(), [
'DD_TRACE_CLI_ENABLED' => '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']
);
}
}
50 changes: 50 additions & 0 deletions tests/ext/inferred_proxy/security_headers_forwarded.phpt
Original file line number Diff line number Diff line change
@@ -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--
<?php
DDTrace\start_span();
DDTrace\close_span();
$spans = dd_trace_serialize_closed_spans();

// The PHP service-entry span has a parent_id pointing to the inferred span;
// the inferred span itself has no parent_id (it is the trace root).
$rootSpan = null;
$inferredSpan = null;
foreach ($spans as $span) {
if (!isset($span['parent_id'])) {
$inferredSpan = $span;
} else {
$rootSpan = $span;
}
}

// Tags must be present on the PHP service-entry span
var_dump($rootSpan['meta']['http.request.headers.x-datadog-endpoint-scan'] ?? 'NOT SET');
var_dump($rootSpan['meta']['http.request.headers.x-datadog-security-test'] ?? 'NOT SET');
// And forwarded to the inferred proxy span
var_dump($inferredSpan['meta']['http.request.headers.x-datadog-endpoint-scan'] ?? 'NOT SET');
var_dump($inferredSpan['meta']['http.request.headers.x-datadog-security-test'] ?? 'NOT SET');
?>
--EXPECT--
string(18) "endpoint-scan-uuid"
string(18) "security-test-uuid"
string(18) "endpoint-scan-uuid"
string(18) "security-test-uuid"
19 changes: 19 additions & 0 deletions tests/ext/root_span_security_testing_headers.phpt
Original file line number Diff line number Diff line change
@@ -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--
<?php
DDTrace\start_span();
DDTrace\close_span(0);
$spans = dd_trace_serialize_closed_spans();
var_dump($spans[0]['meta']['http.request.headers.x-datadog-endpoint-scan']);
var_dump($spans[0]['meta']['http.request.headers.x-datadog-security-test']);
?>
--EXPECT--
string(18) "endpoint-scan-uuid"
string(18) "security-test-uuid"
16 changes: 16 additions & 0 deletions tests/ext/root_span_security_testing_headers_absent.phpt
Original file line number Diff line number Diff line change
@@ -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--
<?php
DDTrace\start_span();
DDTrace\close_span(0);
$spans = dd_trace_serialize_closed_spans();
var_dump(array_key_exists('http.request.headers.x-datadog-endpoint-scan', $spans[0]['meta']));
var_dump(array_key_exists('http.request.headers.x-datadog-security-test', $spans[0]['meta']));
?>
--EXPECT--
bool(false)
bool(false)
Loading