From 40c9ca965b59d78fd5db7f69a29f0ba01824030a Mon Sep 17 00:00:00 2001 From: Craig Potter Date: Sat, 18 Jul 2026 09:23:21 +0100 Subject: [PATCH] Add excluded_response_headers config for redacting response headers Response headers were stored unredacted, including Set-Cookie. The new config mirrors the request-side exclusions: named headers are matched case-insensitively and stored as REDACTED, and '*' or a connector or request class redacts every response header value. Set-Cookie is redacted by default. --- README.md | 15 +++++++++++++++ config/barstool.php | 13 +++++++++++++ src/Barstool.php | 30 ++++++++++++++++++++++++++++- tests/BarstoolTest.php | 43 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a26461b..cd1f59b 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,21 @@ Headers listed in `excluded_request_headers` are stored with their value replace When `'*'` or a connector/request class matches, every header is dropped from the recording except Barstool's own `X-Barstool-UUID` correlation header. +Header names are matched case-insensitively, so `authorization` is caught by an `Authorization` exclusion. + +### Redacting sensitive response headers + +Response headers work the same way via `excluded_response_headers` — matching values are stored as `REDACTED`. The `Set-Cookie` header is redacted by default so session cookies never land in your logs: + +```php +'excluded_response_headers' => [ + 'Set-Cookie', + // '*', // redact every response header + // SensitiveConnector::class, // redact all response headers for a connector + // SensitiveRequest::class, // or a single request +], +``` + ### Excluding response bodies Response bodies for sensitive endpoints can be kept out of the database entirely — they are stored as `REDACTED`: diff --git a/config/barstool.php b/config/barstool.php index ebdbd5c..8449969 100644 --- a/config/barstool.php +++ b/config/barstool.php @@ -82,6 +82,19 @@ // SensitiveConnector::class // Exclude `token` header for this request ], + /* + * Any response headers that should be excluded from the recording. + * Will replace the header value with `REDACTED`. + * Use '*' or a connector/request class name to redact all headers + * for every response or for that connector/request. + */ + 'excluded_response_headers' => [ + 'Set-Cookie', + // '*', // All headers + // SensitiveConnector::class, + // SensitiveRequest::class, + ], + /* * Queue configuration for recording. * When enabled, recordings will be dispatched as queued jobs diff --git a/src/Barstool.php b/src/Barstool.php index a4f95d2..c01f3f4 100755 --- a/src/Barstool.php +++ b/src/Barstool.php @@ -182,13 +182,41 @@ private static function getResponseData(Response $response): array return [ 'url' => $response->getPsrRequest()->getUri(), - 'response_headers' => $response->headers()->all(), + 'response_headers' => self::getResponseHeaders($response), 'response_body' => $responseBody, 'response_status' => $response->status(), 'successful' => $response->successful(), ]; } + /** + * @return array + */ + public static function getResponseHeaders(Response $response): array + { + $excludedHeaders = config('barstool.excluded_response_headers', []); + $headers = collect($response->headers()->all()); + + // '*' or a listed connector/request class redacts every header value + if (in_array('*', $excludedHeaders) + || in_array(get_class($response->getConnector()), $excludedHeaders) + || in_array(get_class($response->getRequest()), $excludedHeaders)) { + return $headers->map(fn () => 'REDACTED')->toArray(); + } + + // Header names are matched case-insensitively, consistent with the + // request-side exclusions. + $excludedHeaders = array_map(mb_strtolower(...), $excludedHeaders); + + return $headers->map(function ($value, $key) use ($excludedHeaders) { + if (in_array(mb_strtolower($key), $excludedHeaders)) { + $value = 'REDACTED'; + } + + return $value; + })->toArray(); + } + /** * @return array{ * url: UriInterface, diff --git a/tests/BarstoolTest.php b/tests/BarstoolTest.php index 29abd73..5eb886e 100644 --- a/tests/BarstoolTest.php +++ b/tests/BarstoolTest.php @@ -1118,3 +1118,46 @@ assertDatabaseCount('barstools', 0); }); + +it('redacts the Set-Cookie response header by default', function () { + config()->set('barstool.enabled', true); + + MockClient::global([ + SoloUserRequest::class => MockResponse::make( + body: ['data' => 'ok'], + status: 200, + headers: ['set-cookie' => 'session=secret-value', 'token' => 'abc123'], + ), + ]); + + (new SoloUserRequest)->send(); + + $barstool = Barstool::sole(); + + expect($barstool->response_headers['set-cookie'])->toBe('REDACTED'); + expect($barstool->response_headers['token'])->toBe('abc123'); +}); + +it('can redact all response headers or by connector or request class', function ($excluded) { + config()->set('barstool.enabled', true); + config()->set('barstool.excluded_response_headers', [$excluded]); + + MockClient::global([ + SoloUserRequest::class => MockResponse::make( + body: ['data' => 'ok'], + status: 200, + headers: ['token' => 'abc123', 'other' => 'value'], + ), + ]); + + (new SoloUserRequest)->send(); + + $barstool = Barstool::sole(); + + expect($barstool->response_headers['token'])->toBe('REDACTED'); + expect($barstool->response_headers['other'])->toBe('REDACTED'); +})->with([ + '*', + NullConnector::class, + SoloUserRequest::class, +]);