PatchClock

PHP security digest · 17 Sep 2026

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

High CVE-2026-76846

Package
getgrav/grav
Fixed in
2.0.16
Published
Thu 17 Sep 2026, 21:27 LisbonUpdated Sun 25 Oct 2026, 23:27 Lisbon
Installs on Packagist
91K

In plain words

A user with page-edit permission can read secrets stored under system.*, such as a Redis cache password, through sandboxed Twig page content. This requires both twig_content.process_enabled and the non-default twig_content.config_access setting to be enabled. The default configuration denylist omits system, leaving those secrets exposed.

Affected versions

PackageAffectedFixed in
getgrav/grav<= 2.0.152.0.16

Am I affected?

Check composer.lock for getgrav/grav at version 2.0.15 or earlier; 2.0.16 is fixed. Check whether both twig_content.process_enabled and twig_content.config_access are true, then inspect twig_sandbox.config_denied_paths in system/config/security.yaml to see whether system is excluded from protection. Check for secrets under system.*, particularly system.cache.redis.password if Redis caching is used, and review who has page-edit permission.

What to do

Check which version you have, then update to a fixed release for your branch (2.0.16):

composer show getgrav/grav | grep versions
composer update getgrav/grav

If your version constraint in composer.json keeps you below a fixed release, raise it first. Whether your configuration is affected, and the exact version ranges, are in the advisory itself.

If you cannot update yet

Add system to twig_sandbox.config_denied_paths in system/config/security.yaml. The advisory also suggests replacing the denylist model with an allowlist containing only confirmed non-sensitive subtrees, such as site.

The advisory's description

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

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:

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
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:

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:

- 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:

{{ config.get('system.cache.redis.password') }}

or

{{ 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)

Weakness: CWE-522 Insufficiently Protected Credentials. Reported by alham-rizvi.

The advisory

From OSV.dev, which gathers the GitHub Advisory Database, FriendsOfPHP and Drupal security advisories; the summary is theirs. The description, affected versions, weakness and credits are from the GitHub Advisory Database (CC BY 4.0). "In plain words", "Am I affected?" and "If you cannot update yet" are based on that advisory, and checked to name no version it does not.