Skip to content

Commit 26bcd92

Browse files
authored
Request / response ip filter (#28)
* New trusted_ips setting for request and response config - to only trust request headers from specific IPs - to only send response headers for requests from specific IPs Turned config trust_request_header into request->trust_header Turned config trust_request_header into response->send_header
1 parent 7d84ab3 commit 26bcd92

File tree

9 files changed

+172
-28
lines changed

9 files changed

+172
-28
lines changed

docs/configuration/tracecontext.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@ return static function (SymfonyTraceConfig $config): void {
1818
// If true a value in the `traceparent` header in the request
1919
// will be used and parsed to get the trace ID for the rest of the request. If false
2020
// those values are ignored and new trace ID's are generated.
21-
$config->trustRequestHeader(true);
21+
$config->request()
22+
->trustHeader(true)
23+
->trustedIps(env('TRUSTED_IPS')); // Only trust the header from these IP's
2224

2325
// Whether to send the trace details in the response headers. This is turned on by default.
24-
$config->sendResponseHeader(true);
26+
$config->response()
27+
->sendHeader(true)
28+
->trustedIps(env('TRUSTED_IPS')); // Only send the header to these IP's
2529

2630
// The service key of an object that implements
2731
// DR\SymfonyTraceBundle\TraceStorageInterface

docs/configuration/traceid.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@ return static function (SymfonyTraceConfig $config): void {
2828
// on by default. If true a value in the `X-Trace-Id` header in the request
2929
// will be used as the trace ID for the rest of the request. If false
3030
// those values are ignored.
31-
$config->trustRequestHeader(true);
31+
$config->request()
32+
->trustHeader(true)
33+
->trustedIps(env('TRUSTED_IPS')); // Only trust the header from these IP's
3234

3335
// Whether to send the trace details in the response headers. This is turned on by default.
34-
$config->sendResponseHeader(true);
36+
$config->response()
37+
->sendHeader(true)
38+
->trustedIps(env('TRUSTED_IPS')); // Only send the header to these IP's
3539

3640
$config->traceid()
3741
// The header which the bundle inspects for the incoming trace ID
@@ -72,3 +76,4 @@ return static function (SymfonyTraceConfig $config): void {
7276
->enabled(true)
7377
->hubService(HubInterface::class);
7478
};
79+
```

phpstan-baseline.neon

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
parameters:
22
ignoreErrors:
33
-
4-
message: "#^Parameter \\#1 \\$mergedConfig \\(array\\{traceMode\\: 'tracecontext'\\|'traceid', traceid\\: array\\{request_header\\: string, response_header\\: string, generator_service\\: string\\|null\\}, trust_request_header\\: bool, send_response_header\\: bool, storage_service\\: string\\|null, enable_monolog\\: bool, enable_console\\: bool, console\\: array\\{enabled\\: bool, trace_id\\: string\\|null\\}, \\.\\.\\.\\}\\) of method DR\\\\SymfonyTraceBundle\\\\DependencyInjection\\\\SymfonyTraceExtension\\:\\:loadInternal\\(\\) should be contravariant with parameter \\$mergedConfig \\(array\\) of method Symfony\\\\Component\\\\HttpKernel\\\\DependencyInjection\\\\ConfigurableExtension\\:\\:loadInternal\\(\\)$#"
4+
message: "#^Parameter \\#1 \\$mergedConfig \\(array\\{traceMode\\: 'tracecontext'\\|'traceid', traceid\\: array\\{request_header\\: string, response_header\\: string, generator_service\\: string\\|null\\}, request\\: array\\{trust_header\\: bool, trusted_ips\\: array\\<string\\>\\|string\\|null\\}, response\\: array\\{send_header\\: bool, trusted_ips\\: array\\<string\\>\\|string\\|null\\}, storage_service\\: string\\|null, enable_monolog\\: bool, enable_console\\: bool, console\\: array\\{enabled\\: bool, trace_id\\: string\\|null\\}, \\.\\.\\.\\}\\) of method DR\\\\SymfonyTraceBundle\\\\DependencyInjection\\\\SymfonyTraceExtension\\:\\:loadInternal\\(\\) should be contravariant with parameter \\$mergedConfig \\(array\\) of method Symfony\\\\Component\\\\HttpKernel\\\\DependencyInjection\\\\ConfigurableExtension\\:\\:loadInternal\\(\\)$#"
55
count: 1
66
path: src/DependencyInjection/SymfonyTraceExtension.php
77

src/DependencyInjection/Configuration.php

+46-8
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,8 @@ public function getConfigTreeBuilder(): TreeBuilder
5353
->end()
5454
->end()
5555
->end()
56-
->booleanNode('trust_request_header')
57-
->defaultTrue()
58-
->info("Whether or not to trust the incoming request's `Trace-Id` header as a real ID")
59-
->end()
60-
->booleanNode('send_response_header')
61-
->defaultTrue()
62-
->info("Whether or not to send a response header with the trace ID. Defaults to true")
63-
->end()
56+
->append($this->createRequestConfiguration())
57+
->append($this->createResponseConfiguration())
6458
->scalarNode('storage_service')
6559
->info('The service name for trace ID storage. Defaults to `TraceStorage`')
6660
->end()
@@ -101,6 +95,50 @@ public function getConfigTreeBuilder(): TreeBuilder
10195
return $tree;
10296
}
10397

98+
private function createRequestConfiguration(): NodeDefinition
99+
{
100+
$node = (new TreeBuilder('request'))->getRootNode();
101+
$node
102+
->addDefaultsIfNotSet()
103+
->children()
104+
->booleanNode('trust_header')
105+
->defaultTrue()
106+
->info("Whether or not to trust the incoming request's headers as a real TraceID")
107+
->end()
108+
->scalarNode('trusted_ips')
109+
->info(
110+
"Only trust incoming request's headers if the request comes from one of these IPs (supports ranges/masks). " .
111+
"Accepts a string-array, comma separated string or null. Defaults to null, accepting all request IPs. "
112+
)
113+
->defaultNull()
114+
->end()
115+
->end();
116+
117+
return $node;
118+
}
119+
120+
private function createResponseConfiguration(): NodeDefinition
121+
{
122+
$node = (new TreeBuilder('response'))->getRootNode();
123+
$node
124+
->addDefaultsIfNotSet()
125+
->children()
126+
->booleanNode('send_header')
127+
->defaultTrue()
128+
->info("Whether or not to send a response header with the trace ID. Defaults to true")
129+
->end()
130+
->scalarNode('trusted_ips')
131+
->info(
132+
"Only send response if the request comes from one of these IPs (supports ranges/masks) " .
133+
"Accepts a string-array, comma separated string or null. Defaults to null, accepting all request IPs. "
134+
)
135+
->defaultNull()
136+
->end()
137+
->end();
138+
139+
return $node;
140+
}
141+
104142
private function createHttpClientConfiguration(): NodeDefinition
105143
{
106144
$node = (new TreeBuilder('http_client'))->getRootNode();

src/DependencyInjection/SymfonyTraceExtension.php

+12-4
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,14 @@
3838
* response_header: string,
3939
* generator_service: ?string,
4040
* },
41-
* trust_request_header: bool,
42-
* send_response_header: bool,
41+
* request: array{
42+
* trust_header: bool,
43+
* trusted_ips: string[]|string|null,
44+
* },
45+
* response: array{
46+
* send_header: bool,
47+
* trusted_ips: string[]|string|null,
48+
* },
4349
* storage_service: ?string,
4450
* enable_monolog: bool,
4551
* enable_console: bool,
@@ -84,8 +90,10 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container
8490
$container->register(TraceSubscriber::class)
8591
->setArguments(
8692
[
87-
$mergedConfig['trust_request_header'],
88-
$mergedConfig['send_response_header'],
93+
$mergedConfig['request']['trust_header'],
94+
$mergedConfig['request']['trusted_ips'],
95+
$mergedConfig['response']['send_header'],
96+
$mergedConfig['response']['trusted_ips'],
8997
new Reference(TraceServiceInterface::class),
9098
new Reference(TraceStorageInterface::class)
9199
]

src/EventSubscriber/TraceSubscriber.php

+31-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use DR\SymfonyTraceBundle\Service\TraceServiceInterface;
88
use DR\SymfonyTraceBundle\TraceStorageInterface;
99
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
10+
use Symfony\Component\HttpFoundation\IpUtils;
11+
use Symfony\Component\HttpFoundation\Request;
1012
use Symfony\Component\HttpKernel\Event\RequestEvent;
1113
use Symfony\Component\HttpKernel\Event\ResponseEvent;
1214
use Symfony\Component\HttpKernel\KernelEvents;
@@ -18,12 +20,16 @@
1820
final class TraceSubscriber implements EventSubscriberInterface
1921
{
2022
/**
21-
* @param bool $trustRequest Trust the value from the request? Or generate?
22-
* @param TraceStorageInterface $traceStorage The trace ID storage, used to store the ID from the request or a newly generated ID.
23+
* @param bool $trustRequest Trust the value from the request? Or generate?
24+
* @param string[]|string|null $trustedIpsRequest The IPs to trust the request from.
25+
* @param string[]|string|null $trustedIpsResponse The IPs to send the response header to.
26+
* @param TraceStorageInterface $traceStorage The trace ID storage, used to store the ID from the request or a newly generated ID.
2327
*/
2428
public function __construct(
2529
private readonly bool $trustRequest,
30+
private readonly array|string|null $trustedIpsRequest,
2631
private readonly bool $sendResponseHeader,
32+
private readonly array|string|null $trustedIpsResponse,
2733
private readonly TraceServiceInterface $traceService,
2834
private readonly TraceStorageInterface $traceStorage,
2935
) {
@@ -35,7 +41,7 @@ public function __construct(
3541
public static function getSubscribedEvents(): array
3642
{
3743
return [
38-
KernelEvents::REQUEST => ['onRequest', 100],
44+
KernelEvents::REQUEST => ['onRequest', 100],
3945
KernelEvents::RESPONSE => ['onResponse', -99],
4046
];
4147
}
@@ -46,10 +52,11 @@ public function onRequest(RequestEvent $event): void
4652
return;
4753
}
4854

49-
$request = $event->getRequest();
55+
$request = $event->getRequest();
56+
$trustRequest = $this->trustRequest && $this->isTrustedRequest($request, $this->trustedIpsRequest);
5057

5158
// If we trust the request, check if the traceService supports it and use the request data
52-
if ($this->trustRequest && $this->traceService->supports($request)) {
59+
if ($trustRequest && $this->traceService->supports($request)) {
5360
$this->traceStorage->setTrace($this->traceService->getRequestTrace($request));
5461

5562
return;
@@ -69,8 +76,26 @@ public function onResponse(ResponseEvent $event): void
6976
return;
7077
}
7178

72-
if ($this->sendResponseHeader) {
79+
$request = $event->getRequest();
80+
$sendHeader = $this->sendResponseHeader && $this->isTrustedRequest($request, $this->trustedIpsResponse);
81+
if ($sendHeader) {
7382
$this->traceService->handleResponse($event->getResponse(), $this->traceStorage->getTrace());
7483
}
7584
}
85+
86+
/**
87+
* @param string[]|string|null $trustedIps
88+
*/
89+
private function isTrustedRequest(Request $request, array|string|null $trustedIps): bool
90+
{
91+
if ($trustedIps === null) {
92+
return true;
93+
}
94+
95+
if (is_string($trustedIps)) {
96+
$trustedIps = array_map('trim', explode(',', $trustedIps));
97+
}
98+
99+
return IpUtils::checkIp((string)$request->getClientIp(), $trustedIps);
100+
}
76101
}

tests/Functional/App/config/tracecontext.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
symfony_trace:
22
traceMode: 'tracecontext'
3-
trust_request_header: true
3+
request:
4+
trust_header: true
45
storage_service: request.id.storage
56
enable_messenger: true
67
http_client:

tests/Functional/App/config/traceid.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ symfony_trace:
33
traceid:
44
request_header: Trace-Id
55
response_header: Trace-Id
6-
trust_request_header: true
6+
request:
7+
trust_header: true
78
storage_service: request.id.storage
89
enable_messenger: true
910
console:

tests/Unit/EventSubscriber/TraceSubscriberTest.php

+65-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ protected function setUp(): void
3434
{
3535
$this->service = $this->createMock(TraceServiceInterface::class);
3636
$this->storage = $this->createMock(TraceStorageInterface::class);
37-
$this->listener = new TraceSubscriber(true, true, $this->service, $this->storage);
37+
$this->listener = new TraceSubscriber(true, null, true, null, $this->service, $this->storage);
3838

3939
$this->dispatcher = new EventDispatcher();
4040
$this->dispatcher->addSubscriber($this->listener);
@@ -74,7 +74,7 @@ public function testListenerSetsTheTraceToStorageWhenFoundInRequestHeaders(): vo
7474
public function testListenerIgnoresIncomingRequestHeadersWhenTrustRequestIsFalse(): void
7575
{
7676
$this->dispatcher->removeSubscriber($this->listener);
77-
$this->dispatcher->addSubscriber(new TraceSubscriber(false, true, $this->service, $this->storage));
77+
$this->dispatcher->addSubscriber(new TraceSubscriber(false, null, true, null, $this->service, $this->storage));
7878

7979
$trace = new TraceContext();
8080
$this->service->expects(static::never())->method('supports');
@@ -138,7 +138,7 @@ public function testRequestSetsIdOnResponse(): void
138138
public function testListenerDoesNothingToResponseWithoutMasterRequestWhenSendResponseHeaderIsFalse(): void
139139
{
140140
$this->dispatcher->removeSubscriber($this->listener);
141-
$this->dispatcher->addSubscriber(new TraceSubscriber(false, false, $this->service, $this->storage));
141+
$this->dispatcher->addSubscriber(new TraceSubscriber(false, null, false, null, $this->service, $this->storage));
142142

143143
$this->storage->expects(static::never())->method('getTrace');
144144
$this->service->expects(static::never())->method('handleResponse');
@@ -148,4 +148,66 @@ public function testListenerDoesNothingToResponseWithoutMasterRequestWhenSendRes
148148
KernelEvents::RESPONSE
149149
);
150150
}
151+
152+
public function testListenerSetsTheTraceToStorageWhenFoundInTrustedRequestHeaders(): void
153+
{
154+
$listener = new TraceSubscriber(true, '127.0.0.1', true, '127.0.0.1', $this->service, $this->storage);
155+
$dispatcher = new EventDispatcher();
156+
$dispatcher->addSubscriber($listener);
157+
158+
$this->service->expects(static::once())->method('getRequestTrace')->with($this->request);
159+
$this->service->expects(static::once())->method('supports')->with($this->request)->willReturn(true);
160+
$this->storage->expects(static::once())->method('setTrace');
161+
162+
$dispatcher->dispatch(
163+
new RequestEvent($this->kernel, $this->request, HttpKernelInterface::MAIN_REQUEST),
164+
KernelEvents::REQUEST
165+
);
166+
}
167+
168+
public function testListenerIgnoresRequestHeadersOnNonTrustedRequest(): void
169+
{
170+
$listener = new TraceSubscriber(true, '127.0.0.2', true, '127.0.0.2', $this->service, $this->storage);
171+
$dispatcher = new EventDispatcher();
172+
$dispatcher->addSubscriber($listener);
173+
174+
$this->service->expects(static::never())->method('supports');
175+
$this->service->expects(static::never())->method('getRequestTrace');
176+
177+
$dispatcher->dispatch(
178+
new RequestEvent($this->kernel, $this->request, HttpKernelInterface::MAIN_REQUEST),
179+
KernelEvents::REQUEST
180+
);
181+
}
182+
183+
public function testRequestSetsIdOnResponseOnTrustedIp(): void
184+
{
185+
$listener = new TraceSubscriber(true, '127.0.0.1', true, '127.0.0.1', $this->service, $this->storage);
186+
$dispatcher = new EventDispatcher();
187+
$dispatcher->addSubscriber($listener);
188+
189+
$trace = new TraceContext();
190+
$this->storage->expects(static::once())->method('getTrace')->willReturn($trace);
191+
$this->service->expects(static::once())->method('handleResponse')->with($this->response, $trace);
192+
193+
$dispatcher->dispatch(
194+
new ResponseEvent($this->kernel, $this->request, HttpKernelInterface::MAIN_REQUEST, $this->response),
195+
KernelEvents::RESPONSE
196+
);
197+
}
198+
199+
public function testRequestDoesNotSetIdOnResponseOnNonTrustedIp(): void
200+
{
201+
$listener = new TraceSubscriber(true, '127.0.0.2', true, '127.0.0.2', $this->service, $this->storage);
202+
$dispatcher = new EventDispatcher();
203+
$dispatcher->addSubscriber($listener);
204+
205+
$this->storage->expects(static::never())->method('getTrace');
206+
$this->service->expects(static::never())->method('handleResponse');
207+
208+
$dispatcher->dispatch(
209+
new ResponseEvent($this->kernel, $this->request, HttpKernelInterface::MAIN_REQUEST, $this->response),
210+
KernelEvents::RESPONSE
211+
);
212+
}
151213
}

0 commit comments

Comments
 (0)