Skip to content

Conversation

onlime
Copy link
Contributor

@onlime onlime commented Mar 25, 2025

The current set of default request-logger.filters in config/request-logger.php is quite misleading:

    'filters' => [
        'password',
        'password_confirm',
        'apikey',
        'api_token',
        'Authorization', // 💥
        'filter.search',
    ],

You could assume that Request header keys are Pascal-Cased, as the sample filters contain Authorization. This is widely used, even though the standards say they are case insensitive and should be lowercased.

In my case, this led to a security issue, as none of my webhook's authorization headers were masked in RequestLog model's headers attribute. The current implementation of RequestLog::replaceParameters() is case-sensitive, but the Request headers were actually lowercased. Arr::get() and Arr::set() are also case-sensitive.

As a workaround, I am now search-replacing both variants, the original filter key plus its lowercased version. So we can safely leave Authorization in the default config.

In addition, this PR now masks the filtered values by the same length of asterisks (BEFORE: fixed-length ********), for improved debugging.

@onlime
Copy link
Contributor Author

onlime commented Mar 25, 2025

and for the ones that suffer from the same flaw, without destroying all request_log data, you may sanitize it like this:

UPDATE `request_logs` SET `headers` = REGEXP_REPLACE(`headers`, 'Bearer:?\s*[^"]+', '********') WHERE `headers` LIKE '%authorization%';

Copy link
Owner

@bilfeldt bilfeldt left a comment

Choose a reason for hiding this comment

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

Thanks for the PR @onlime 👍

The masking "pattern/lenght" is a nice addition. Regarding the case-sensitivity then I think it makes sense making the filtering case-insensitive, but we need to make sure that fields names are not converted in the process.

Comment on lines 184 to 187
foreach ([$param, mb_strtolower($param)] as $key) {
if ($value = Arr::get($array, $key)) {
Arr::set($array, $key, str_repeat($maskChar, mb_strlen($value)));
}
Copy link
Owner

Choose a reason for hiding this comment

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

Doe this work when Arr::get() and Arr::get() are case sensitive? 🤔 What if for example the field is fooBar but the filter applied is for foobar?

It is important that if the field was 'fooBar' => 'secret' in the data then it should be 'fooBar' => '******' in the filtered data, not be converted to 'foobar' => '******'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wanted to keep the luxury of Arr::get() and Arr::set(), but they are case-sensitive, as they just use regular PHP array access by key, which is always case sensitive. In my proposed solution, the original key would never be changed. But 'fooBar' => 'secret' would not get masked at all, if config('request-logger.filters') contain foobar. So it's just a workaround for most cases, but not a complete case-insensitive solution.

We would either need to write an Arr macro of a case-insensitive Arr::get() variant (so we could keep the luxury of dotted notation key access of nested items), or give up on Arr::get() and implement a simpler solution that does it fully case-insensitive but without dotted notation support. I am not 100% sure if dotted notation is needed, but in RequestLog::getLoggableResponseContent() it looks like it is, as you're masking the whole json decoded response content. With dotted notation support, it might get quite tricky and would make more sense to PR this to the framework.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @bilfeldt it took me quite a bit longer, as there is no easy solution for this. I have committed a Arr::maskCaseInsensitive() macro in 01e5ef0, which should handle all use cases. Personally, I have tested this in my project, so if you're happy with this solution, please go ahead and port my (PEST) tests:

it('masks parts of an array by case-insensitive key matching', function (
    array $array,
    array $keys,
    array $expected,
    string $mask = '*'
) {
    expect(Arr::maskCaseInsensitive($array, $keys, $mask))->toBe($expected);
})->with([
    'mask-some' => [
        ['foo' => 123, 'bar' => 'some@thing', 'baz' => ['aaa', 'bbb', 456]],
        ['bar', 'baz.0', 'baz.2'],
        ['foo' => 123, 'bar' => '**********', 'baz' => ['***', 'bbb', '***']],
    ],
    'mask-sub-array' => [
        ['foo' => 123, 'bar' => ['aaa', 'bbb', 456]],
        ['bar'],
        ['foo' => 123, 'bar' => '********'],
    ],
    'mask-empty-values' => [
        ['foo' => [], 'bar' => null, 'baz' => ['key1' => 0]],
        ['foo', 'bar', 'baz.key1'],
        ['foo' => [], 'bar' => null, 'baz' => ['key1' => '*']],
    ],
    'mask-case-insensitive' => [
        ['Authorization' => 'Bearer 1234', 'fooBar' => ['Baz' => 123], 'filter' => ['Search' => 'abc']],
        ['authorization', 'Foobar.baz', 'filter.search'],
        ['Authorization' => '***********', 'fooBar' => ['Baz' => '***'], 'filter' => ['Search' => '***']],
    ],
    'mask-fixed-length' => [
        ['foo' => 123, 'bar' => 'some@thing', 'baz' => ['aaa', 'bbb', 456]],
        ['bar', 'baz.0', 'baz.2'],
        ['foo' => 123, 'bar' => 'xxxxxxxx', 'baz' => ['xxxxxxxx', 'bbb', 'xxxxxxxx']],
        'xxxxxxxx',
    ],
]);

onlime added 2 commits March 30, 2025 15:05
Only use value to mask by string length if single char is provided, otherwise use it as replacement
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants