From 4ce20c6fc6e5e1d978697127d4280f6b0a618e80 Mon Sep 17 00:00:00 2001 From: ernolf Date: Sun, 19 Jul 2026 02:43:19 +0200 Subject: [PATCH] fix: use the server's sensitive-value placeholder and keep the private- marker - redacted headers, query parameters and path portions now read ***REMOVED SENSITIVE VALUE*** (IConfig::SENSITIVE_VALUE), matching the server's own redaction convention - the built-in iCal path pattern redacts only the hash after private-, so the marker stays readable in the logs Signed-off-by: ernolf --- README.md | 8 ++--- .../Middleware/HttpClientLoggerMiddleware.php | 26 ++++++++++------ .../HttpClientLoggerMiddlewareTest.php | 30 +++++++++---------- 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index deed63d..2b9faa9 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ List of hostnames to exclude from logging. Requests to matching hosts are passed ### `audit_http_client_redact_headers` -Additional header names whose values are logged as `[redacted]`. Matching is case-insensitive. Default: `[]`. +Additional header names whose values are logged as `***REMOVED SENSITIVE VALUE***`. Matching is case-insensitive. Default: `[]`. The following headers are always redacted and cannot be un-redacted: `Authorization`, `Proxy-Authorization`, `Cookie`, `Set-Cookie`, `X-Api-Key`, `X-Auth-Token`. @@ -100,7 +100,7 @@ The following headers are always redacted and cannot be un-redacted: `Authorizat ### `audit_http_client_redact_params` -Additional query parameter names whose values are logged as `[redacted]` in the `uri` field. Matching is case-insensitive; the parameter name and the rest of the URI stay unchanged. Default: `[]`. +Additional query parameter names whose values are logged as `***REMOVED SENSITIVE VALUE***` in the `uri` field. Matching is case-insensitive; the parameter name and the rest of the URI stay unchanged. Default: `[]`. The following parameters are always redacted and cannot be un-redacted: `access_token`, `api_key`, `apikey`, `auth`, `client_secret`, `password`, `secret`, `signature`, `token`, `X-Amz-Credential`, `X-Amz-Security-Token`, `X-Amz-Signature`. @@ -112,9 +112,9 @@ The following parameters are always redacted and cannot be un-redacted: `access_ ### `audit_http_client_redact_path_patterns` -Additional PCRE patterns whose matches in the URI **path** are logged as `[redacted]` — for secrets that live in the path instead of the query string. Default: `[]`. +Additional PCRE patterns whose matches in the URI **path** are logged as `***REMOVED SENSITIVE VALUE***` — for secrets that live in the path instead of the query string. Default: `[]`. -The following pattern is always applied and cannot be removed: `#(?<=/)private-[^/]+#` — it covers Google's secret iCal addresses (`…/ical//private-/basic.ics`), where the hash is the only protection of the calendar. +The following pattern is always applied and cannot be removed: `#(?<=/private-)[^/]+#` — it covers Google's secret iCal addresses (`…/ical//private-/basic.ics`), where the hash is the only protection of the calendar. Only the hash is replaced; the `private-` marker stays readable. ```php 'audit_http_client_redact_path_patterns' => [ diff --git a/lib/Http/Client/Middleware/HttpClientLoggerMiddleware.php b/lib/Http/Client/Middleware/HttpClientLoggerMiddleware.php index b1cdc0d..5fb29de 100644 --- a/lib/Http/Client/Middleware/HttpClientLoggerMiddleware.php +++ b/lib/Http/Client/Middleware/HttpClientLoggerMiddleware.php @@ -11,14 +11,21 @@ use GuzzleHttp\RequestOptions; use GuzzleHttp\TransferStats; +use OCP\IConfig; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; class HttpClientLoggerMiddleware { /** - * Header values that would leak credentials to disk; always logged as - * [redacted], with no opt-out. + * Same placeholder the server itself uses when it strips sensitive + * values from config output. + */ + private const REDACTED = IConfig::SENSITIVE_VALUE; + + /** + * Header values that would leak credentials to disk; always replaced by + * IConfig::SENSITIVE_VALUE, with no opt-out. */ private const DEFAULT_REDACT_HEADERS = [ 'authorization', @@ -31,7 +38,7 @@ class HttpClientLoggerMiddleware { /** * Query parameter values that would leak credentials to disk when the URI - * is logged; always logged as [redacted], with no opt-out. + * is logged; always replaced by IConfig::SENSITIVE_VALUE, with no opt-out. */ private const DEFAULT_REDACT_PARAMS = [ 'access_token', @@ -49,12 +56,13 @@ class HttpClientLoggerMiddleware { ]; /** - * PCRE patterns whose matches in the URI path are logged as [redacted]. + * PCRE patterns whose matches in the URI path are replaced by + * IConfig::SENSITIVE_VALUE. * Google marks its secret iCal URLs with a "private-" path segment; * anyone holding such a URL can read the calendar. */ private const DEFAULT_REDACT_PATH_PATTERNS = [ - '#(?<=/)private-[^/]+#', + '#(?<=/private-)[^/]+#', ]; private LoggerInterface $logger; @@ -390,7 +398,7 @@ private function isExcluded(string $host): bool { /** * Redacts credentials from a URI before it is logged: values of - * credential-bearing query parameters become [redacted] (names keep their + * credential-bearing query parameters become IConfig::SENSITIVE_VALUE (names keep their * original spelling, matching is case-insensitive, parameters without a * value and the fragment are left untouched), and path portions matching * the configured patterns are replaced likewise. Invalid patterns are @@ -401,7 +409,7 @@ private function redactUri(string $uri): string { $base = $qPos === false ? $uri : substr($uri, 0, $qPos); foreach ($this->redactPathPatterns as $pattern) { - $replaced = @preg_replace($pattern, '[redacted]', $base); + $replaced = @preg_replace($pattern, self::REDACTED, $base); if (is_string($replaced)) { $base = $replaced; } @@ -428,7 +436,7 @@ private function redactUri(string $uri): string { } $name = substr($pair, 0, $eq); if (in_array(strtolower(rawurldecode($name)), $this->redactParams, true)) { - $pairs[$i] = $name . '=[redacted]'; + $pairs[$i] = $name . '=' . self::REDACTED; } } @@ -452,7 +460,7 @@ private function compactHeaders(array $hdrs): array { $lk = strtolower($k); if (in_array($lk, $this->redactHeaders, true)) { - $out[$k] = '[redacted]'; + $out[$k] = self::REDACTED; continue; } diff --git a/tests/unit/Http/Client/Middleware/HttpClientLoggerMiddlewareTest.php b/tests/unit/Http/Client/Middleware/HttpClientLoggerMiddlewareTest.php index 030b7fa..2667b6f 100644 --- a/tests/unit/Http/Client/Middleware/HttpClientLoggerMiddlewareTest.php +++ b/tests/unit/Http/Client/Middleware/HttpClientLoggerMiddlewareTest.php @@ -236,12 +236,12 @@ public function testCompactHeadersRedactsSensitiveDefaultsCaseInsensitive(): voi $mw = $this->middleware(); $this->assertSame( [ - 'Authorization' => '[redacted]', - 'Proxy-Authorization' => '[redacted]', - 'Cookie' => '[redacted]', - 'SET-COOKIE' => '[redacted]', - 'x-api-key' => '[redacted]', - 'X-Auth-Token' => '[redacted]', + 'Authorization' => '***REMOVED SENSITIVE VALUE***', + 'Proxy-Authorization' => '***REMOVED SENSITIVE VALUE***', + 'Cookie' => '***REMOVED SENSITIVE VALUE***', + 'SET-COOKIE' => '***REMOVED SENSITIVE VALUE***', + 'x-api-key' => '***REMOVED SENSITIVE VALUE***', + 'X-Auth-Token' => '***REMOVED SENSITIVE VALUE***', 'Accept' => 'text/html', ], $this->invokePrivate($mw, 'compactHeaders', [[ @@ -260,9 +260,9 @@ public function testCompactHeadersRedactsConfiguredExtraHeaders(): void { $mw = $this->middleware(0, [], ['X-Secret', ' x-internal ']); $this->assertSame( [ - 'X-Secret' => '[redacted]', - 'X-Internal' => '[redacted]', - 'Authorization' => '[redacted]', + 'X-Secret' => '***REMOVED SENSITIVE VALUE***', + 'X-Internal' => '***REMOVED SENSITIVE VALUE***', + 'Authorization' => '***REMOVED SENSITIVE VALUE***', 'Accept' => 'text/html', ], $this->invokePrivate($mw, 'compactHeaders', [[ @@ -277,11 +277,11 @@ public function testCompactHeadersRedactsConfiguredExtraHeaders(): void { public function testRedactUriRedactsDefaultParamsCaseInsensitively(): void { $mw = $this->middleware(); $this->assertSame( - 'https://api.example.com/v1/data?access_token=[redacted]&page=2', + 'https://api.example.com/v1/data?access_token=***REMOVED SENSITIVE VALUE***&page=2', $this->invokePrivate($mw, 'redactUri', ['https://api.example.com/v1/data?access_token=geheim&page=2']), ); $this->assertSame( - 'https://api.example.com/v1/data?Access_Token=[redacted]', + 'https://api.example.com/v1/data?Access_Token=***REMOVED SENSITIVE VALUE***', $this->invokePrivate($mw, 'redactUri', ['https://api.example.com/v1/data?Access_Token=geheim']), ); } @@ -289,7 +289,7 @@ public function testRedactUriRedactsDefaultParamsCaseInsensitively(): void { public function testRedactUriRedactsConfiguredExtraParams(): void { $mw = $this->middleware(0, [], [], ['session_key']); $this->assertSame( - 'https://x.test/p?session_key=[redacted]&token=[redacted]', + 'https://x.test/p?session_key=***REMOVED SENSITIVE VALUE***&token=***REMOVED SENSITIVE VALUE***', $this->invokePrivate($mw, 'redactUri', ['https://x.test/p?session_key=abc&token=xyz']), ); } @@ -305,7 +305,7 @@ public function testRedactUriLeavesValuelessParamsAndFragment(): void { public function testRedactUriRedactsPrivatePathSegments(): void { $mw = $this->middleware(); $this->assertSame( - 'https://calendar.google.com/calendar/ical/user%40googlemail.com/[redacted]/basic.ics', + 'https://calendar.google.com/calendar/ical/user%40googlemail.com/private-***REMOVED SENSITIVE VALUE***/basic.ics', $this->invokePrivate($mw, 'redactUri', [ 'https://calendar.google.com/calendar/ical/user%40googlemail.com/private-0123456789abcdef0123456789abcdef/basic.ics', ]), @@ -315,7 +315,7 @@ public function testRedactUriRedactsPrivatePathSegments(): void { public function testRedactUriAppliesConfiguredPathPatterns(): void { $mw = $this->middleware(0, [], [], [], ['#(?<=/)key-[0-9a-f]+#']); $this->assertSame( - 'https://x.test/feed/[redacted]/data.xml', + 'https://x.test/feed/***REMOVED SENSITIVE VALUE***/data.xml', $this->invokePrivate($mw, 'redactUri', ['https://x.test/feed/key-abc123/data.xml']), ); } @@ -350,7 +350,7 @@ public function testLoggedUriIsRedacted(): void { $lines = file($dir . '/example.com.json', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $this->assertNotFalse($lines); $entry = json_decode($lines[0], true, 512, JSON_THROW_ON_ERROR); - $this->assertSame('https://example.com/hook?token=[redacted]&id=7', $entry['uri']); + $this->assertSame('https://example.com/hook?token=***REMOVED SENSITIVE VALUE***&id=7', $entry['uri']); } finally { foreach (glob($dir . '/*') ?: [] as $file) { @unlink($file);