From a220e9f88cb7efaa05caba91b239b1995f72b406 Mon Sep 17 00:00:00 2001 From: David Palmer Date: Thu, 10 Sep 2026 15:04:58 -0400 Subject: [PATCH 1/2] Redact Secret-Bearing Environment Variables phpinfo() prints the whole process environment three times: as APP_KEY under Environment, and as $_ENV['APP_KEY'] and $_SERVER['APP_KEY'] under PHP Variables. On a Laravel app that renders the encryption key, the database password and every third-party credential on one page. Redaction::value() replaces the value of a variable whose name contains a sensitive pattern. The row stays, so the page still shows which variables are set. Matching is scoped to the two environment modules, so PHP settings such as "Max keys", "Tokenizer Support" and "highlight.keyword" keep their values. Config controls the behaviour: redact-environment turns it off, redact-patterns changes the set, redact-placeholder changes the text. The keys are flat, so mergeConfigFrom gives an already-published config file the new defaults. Co-Authored-By: Claude Opus 5 --- README.md | 28 ++++++-- config/filament-phpinfo.php | 24 +++++++ resources/views/phpinfo.blade.php | 8 ++- src/Redaction.php | 96 +++++++++++++++++++++++++++ tests/RedactionTest.php | 106 ++++++++++++++++++++++++++++++ 5 files changed, 254 insertions(+), 8 deletions(-) create mode 100644 src/Redaction.php create mode 100644 tests/RedactionTest.php diff --git a/README.md b/README.md index 4b8f0ed..582d93c 100644 --- a/README.md +++ b/README.md @@ -23,18 +23,36 @@ $panel ]) ``` +## Secrets + +`phpinfo()` prints the whole process environment three times: once under `Environment` as +`APP_KEY`, and twice under `PHP Variables` as `$_ENV['APP_KEY']` and `$_SERVER['APP_KEY']`. On a +Laravel app that puts the encryption key, the database password and every third-party credential +on one page. + +This package replaces those values with `[redacted]` by default. It keeps the row, so the page +still tells you which variables are set. Matching applies only to the two environment modules, so +PHP settings such as `Max keys` and `Tokenizer Support` keep their values. + +The page is still worth gating to your most trusted users. Redaction removes the credentials, not +the rest of the host's configuration. + ## Configuration -The navigation group and icon are configurable. +The navigation group, icon, and secret redaction are configurable. Publish the `filament-phpinfo` config file with: ```bash php artisan vendor:publish --tag=filament-phpinfo-config ``` -| Option | Description | -|--------------------|----------------------------------------------------------------------------------------------------------------------| -| `navigation-group` | The PHPInfo page's [navigation group](https://filamentphp.com/docs/3.x/panels/navigation#grouping-navigation-items). | -| `navigation-icon` | The PHPInfo page's icon. See Filament's [documentation](https://filamentphp.com/docs/3.x/support/icons) for values. | +| Option | Description | +|-----------------------|----------------------------------------------------------------------------------------------------------------------| +| `navigation-group` | The PHPInfo page's [navigation group](https://filamentphp.com/docs/3.x/panels/navigation#grouping-navigation-items). | +| `navigation-icon` | The PHPInfo page's icon. See Filament's [documentation](https://filamentphp.com/docs/3.x/support/icons) for values. | +| `page-slug` | The PHPInfo page's URL slug. | +| `redact-environment` | Whether to replace the values of secret-bearing environment variables. Defaults to `true`. | +| `redact-patterns` | The strings that mark an environment variable name as secret-bearing. Matching ignores case. | +| `redact-placeholder` | What a redacted value shows instead. Defaults to `[redacted]`. | | Screenshot | |---| diff --git a/config/filament-phpinfo.php b/config/filament-phpinfo.php index 2ffb9bb..1bbc041 100644 --- a/config/filament-phpinfo.php +++ b/config/filament-phpinfo.php @@ -1,7 +1,31 @@ 'System Management', 'navigation-icon' => 'heroicon-o-information-circle', 'page-slug' => 'phpinfo', + + /* + * phpinfo() prints the whole process environment three times, under Environment as + * APP_KEY and under PHP Variables as $_ENV['APP_KEY'] and $_SERVER['APP_KEY']. On a + * Laravel app that puts the encryption key, the database password and every third-party + * credential on one page. Set this to false to print the values. + */ + 'redact-environment' => true, + + /* + * An environment variable whose name contains any of these strings has its value replaced. + * Matching ignores case, and applies only to the two environment modules above, so PHP + * settings such as "Max keys" and "Tokenizer Support" keep their values. Replace this with + * your own array to change the set. + */ + 'redact-patterns' => Redaction::DEFAULT_PATTERNS, + + /* + * What a redacted value shows instead. The row itself stays, so the page still tells you + * which variables are set. + */ + 'redact-placeholder' => Redaction::DEFAULT_PLACEHOLDER, ]; diff --git a/resources/views/phpinfo.blade.php b/resources/views/phpinfo.blade.php index f84b751..1d09d5c 100644 --- a/resources/views/phpinfo.blade.php +++ b/resources/views/phpinfo.blade.php @@ -1,3 +1,5 @@ +@use(STS\FilamentPHPInfo\Redaction) + @foreach ($info->modules() as $module) @@ -22,13 +24,13 @@
- {{ $config->localValue() }} + {{ Redaction::value($module->name(), $config->name(), $config->localValue()) }}
- {{ $config->masterValue() }} + {{ Redaction::value($module->name(), $config->name(), $config->masterValue()) }}
@@ -42,7 +44,7 @@
- {{ $config->localValue() }} + {{ Redaction::value($module->name(), $config->name(), $config->localValue()) }}
diff --git a/src/Redaction.php b/src/Redaction.php new file mode 100644 index 0000000..c52320a --- /dev/null +++ b/src/Redaction.php @@ -0,0 +1,96 @@ + + */ + public static function patterns(): array + { + return config('filament-phpinfo.redact-patterns') ?? static::DEFAULT_PATTERNS; + } + + /** + * phpinfo() prints the same variable three times, as APP_KEY under Environment and as + * $_ENV['APP_KEY'] and $_SERVER['APP_KEY'] under PHP Variables. Reduce all three forms to + * the bare variable name before matching. + */ + protected static function variableName(string $name): string + { + $name = trim($name); + + if (preg_match('/^\$_[A-Z]+\[[\'"]?(.*?)[\'"]?\]$/', $name, $matches)) { + $name = $matches[1]; + } + + return strtoupper($name); + } +} diff --git a/tests/RedactionTest.php b/tests/RedactionTest.php new file mode 100644 index 0000000..a68ec1a --- /dev/null +++ b/tests/RedactionTest.php @@ -0,0 +1,106 @@ +toBe('[redacted]'); +}); + +it('redacts every row form phpinfo prints for one variable', function ($module, $name) { + expect(Redaction::isSensitive($module, $name))->toBeTrue(); +})->with([ + ['Environment', 'APP_KEY'], + ['PHP Variables', "\$_ENV['APP_KEY']"], + ['PHP Variables', '$_SERVER["APP_KEY"]'], +]); + +it('redacts on every pattern', function ($name) { + expect(Redaction::isSensitive('Environment', $name))->toBeTrue(); +})->with([ + 'AWS_SECRET_ACCESS_KEY', + 'DB_PASSWORD', + 'NIGHTWATCH_TOKEN', + 'GOOGLE_APPLICATION_CREDENTIALS', + 'OAUTH_PRIVATE_KEY', + 'JWT_SIGNING_SALT', + 'JWT_RECIPIENT_SIGNATURE', + 'SENTRY_DSN', + 'NOVA_LICENSE_KEY', + 'SLACK_WEBHOOK', +]); + +it('keeps ordinary environment variables', function ($name) { + expect(Redaction::value('Environment', $name, 'kept'))->toBe('kept'); +})->with(['APP_ENV', 'DB_HOST', 'MAIL_FROM_ADDRESS', 'AWS_DEFAULT_REGION', 'PATH']); + +it('keeps php settings whose name matches a pattern', function ($module, $name) { + expect(Redaction::value($module, $name, 'kept'))->toBe('kept'); +})->with([ + ['Zend OPcache', 'Max keys'], + ['Zend OPcache', 'Cached keys'], + ['Zend OPcache', 'Hash keys restarts'], + ['tokenizer', 'Tokenizer Support'], + ['Core', 'highlight.keyword'], +]); + +it('leaves an unset variable alone, so the page still shows it is empty', function ($value) { + expect(Redaction::value('Environment', 'APP_KEY', $value))->toBe($value); +})->with([null, '']); + +it('can be turned off', function () { + config()->set('filament-phpinfo.redact-environment', false); + + expect(Redaction::value('Environment', 'APP_KEY', 'base64:abc123'))->toBe('base64:abc123'); +}); + +it('respects custom patterns from config', function () { + config()->set('filament-phpinfo.redact-patterns', ['tenant']); + + expect(Redaction::value('Environment', 'TENANT_ID', 'acme'))->toBe('[redacted]'); + expect(Redaction::value('Environment', 'APP_KEY', 'base64:abc123'))->toBe('base64:abc123'); +}); + +it('respects a custom placeholder from config', function () { + config()->set('filament-phpinfo.redact-placeholder', '***'); + + expect(Redaction::value('Environment', 'APP_KEY', 'base64:abc123'))->toBe('***'); +}); + +it('redacts nothing outside the environment modules of a real capture', function () { + $redacted = []; + + foreach (Info::capture()->modules() as $module) { + if (in_array($module->name(), Redaction::ENVIRONMENT_MODULES, true)) { + continue; + } + + foreach ($module->configs() as $config) { + if (Redaction::isSensitive($module->name(), $config->name())) { + $redacted[] = $module->name() . ' / ' . $config->name(); + } + } + } + + expect($redacted)->toBeEmpty(); +}); + +it('redacts a secret in a real capture', function () { + putenv('FILAMENT_PHPINFO_TEST_SECRET=must-not-render'); + + $names = []; + + foreach (Info::capture()->modules() as $module) { + foreach ($module->configs() as $config) { + if (str_contains($config->name(), 'FILAMENT_PHPINFO_TEST_SECRET')) { + $names[] = $config->name(); + expect(Redaction::value($module->name(), $config->name(), $config->localValue())) + ->toBe('[redacted]'); + } + } + } + + expect($names)->not->toBeEmpty(); + + putenv('FILAMENT_PHPINFO_TEST_SECRET'); +}); From c892a3da053ca98f081f58c47ea5b42d8cc04d4c Mon Sep 17 00:00:00 2001 From: David Palmer Date: Thu, 10 Sep 2026 15:10:59 -0400 Subject: [PATCH 2/2] Unblock The Laravel 11 CI Legs Laravel 11 is past its security-support window, so Composer now refuses to load any 11.x release and both Laravel 11 legs die at composer update. The failure predates this branch: origin/main resolves the same matrix leg the same way. Those legs check that the package still works on Laravel 11, not that Laravel 11 is patched, so turn off advisory blocking for them alone. Every other leg keeps it. Co-Authored-By: Claude Opus 5 --- .github/workflows/tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a6513f7..9f2e43f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -70,6 +70,12 @@ jobs: - name: Install dependencies run: | + # Laravel 11 is past its security-support window, so Composer refuses to load any + # 11.x release. This leg checks that the package still works on Laravel 11, not that + # Laravel 11 is patched, so let the resolver see it. + if [[ "${{ matrix.laravel }}" == "11.*" ]]; then + composer config --no-plugins policy.advisories.block false + fi composer require "filament/filament:${{ matrix.filament }}" "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update if [[ "${{ matrix.laravel }}" != "13.*" ]]; then composer require "pestphp/pest-plugin-laravel:^3.0" --no-interaction --no-update