Skip to content
Merged
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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand All @@ -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`.

Expand All @@ -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/<user>/private-<hash>/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/<user>/private-<hash>/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' => [
Expand Down
26 changes: 17 additions & 9 deletions lib/Http/Client/Middleware/HttpClientLoggerMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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-<hash>" path segment;
* anyone holding such a URL can read the calendar.
*/
private const DEFAULT_REDACT_PATH_PATTERNS = [
'#(?<=/)private-[^/]+#',
'#(?<=/private-)[^/]+#',
];

private LoggerInterface $logger;
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand All @@ -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;
}
}

Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', [[
Expand All @@ -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', [[
Expand All @@ -277,19 +277,19 @@ 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']),
);
}

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']),
);
}
Expand All @@ -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',
]),
Expand All @@ -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']),
);
}
Expand Down Expand Up @@ -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);
Expand Down
Loading