Grav API Key Scope Bypass in ApiKeyAuthenticator
CVE-2026-62231: Grav API plugin ignores key scopes, letting a read-only API key perform full administrative operations. CVSS 8.1 HIGH.
Overview
CVE-2026-62231 is an authorization bypass in the Grav CMS API plugin (getgrav/grav-plugin-api) affecting all versions prior to 1.0.6. The plugin allows administrators to provision API keys with a restricted scopes array — a capability that implies granular, least-privilege access control. In practice, however, the ApiKeyAuthenticator class responsible for authenticating incoming requests with those keys never reads the scopes field. Instead, it loads the full account object of the key’s owning user and returns it directly to the request pipeline, effectively granting every request the full set of permissions that user possesses, regardless of what scopes were declared on the key.
The vulnerability was identified by security researchers reviewing the plugin’s authentication flow and reported through coordinated disclosure. It affects any Grav installation that has deployed the API plugin and relies on scoped API keys for third-party integrations, CI/CD pipelines, or delegated access — a common pattern in headless CMS deployments.
The practical blast radius depends entirely on the privilege level of the key-owning user. A key issued to an administrative account and shared with a read-only integration effectively becomes a full administrative credential, with no technical enforcement preventing escalation. Organizations running Grav as a headless backend for content delivery, where API keys are distributed to external consumers, face the highest exposure.
Technical Analysis
The root cause is a straightforward case of incomplete implementation: the scopes field is persisted but never evaluated. When a request arrives bearing an API key, ApiKeyAuthenticator performs the following logic:
// VULNERABLE — ApiKeyAuthenticator.php (< 1.0.6)
class ApiKeyAuthenticator
{
public function authenticate(Request $request): ?User
{
$apiKey = $request->headers->get('X-API-Key');
if (!$apiKey) {
return null;
}
// Iterate stored keys looking for a match
foreach ($this->keyStore->all() as $keyRecord) {
if (hash_equals($keyRecord['key'], $apiKey)) {
// Load and return the full user — scopes are NEVER checked
return $this->userFactory->load($keyRecord['owner']);
}
}
return null;
}
}
The $keyRecord array contains a scopes field (e.g., ['read:pages']) populated at key-creation time, but the authenticator ignores it entirely. The call to $this->userFactory->load($keyRecord['owner']) retrieves the owning user’s complete account object — including all roles, permissions, and group memberships — and returns it as the authenticated principal for the request.
From that point forward, Grav’s normal permission system applies against the user object, not against any key-level scope filter. Because the user object carries full permissions, every authorization check downstream passes. A key provisioned with scopes: ['read:pages'] can issue POST /api/pages, DELETE /api/users/{id}, or PUT /api/admin/config without any enforcement barrier.
This maps directly to CWE-862 (Missing Authorization) and represents a textbook example of what OWASP API Security Top 10 categorizes as API5:2023 — Broken Function Level Authorization. The feature surface (scoped keys) was implemented at the data layer without a corresponding enforcement layer in the authentication and authorization pipeline.
Impact
An attacker — or any consumer — holding a legitimately issued, low-privilege API key can:
- Create, modify, or delete content regardless of the
read-only scope declared on their key. - Create or delete user accounts if the key owner is an administrator.
- Modify site configuration, including security-sensitive settings such as authentication providers, plugin configuration, and stream mappings.
- Exfiltrate any data accessible through the API, including user tables and private content.
From a CVSS v3.1 perspective, the score of 8.1 (HIGH) reflects: Attack Vector: Network, Attack Complexity: Low, Privileges Required: Low (a valid key is required), User Interaction: None, Scope: Unchanged, Confidentiality: High, Integrity: High, Availability: None. The requirement for a pre-existing API key is the sole mitigating factor preventing a Critical rating. In any realistic deployment where API keys are distributed externally — to build systems, CDN hooks, or third-party integrators — that bar is already cleared.
Headless Grav deployments are the highest-risk scenario. These architectures routinely issue scoped keys to external consumers under the assumption of least-privilege enforcement. That assumption is false in all versions prior to 1.0.6.
How to Fix It
Upgrade immediately to version 1.0.6 of getgrav/grav-plugin-api:
# Using Composer
composer update getgrav/grav-plugin-api
# Or pin to the patched version explicitly
composer require getgrav/grav-plugin-api:^1.0.6
The correct fix requires the authenticator to resolve the active request’s required scope, intersect it with the key’s declared scopes, and reject the request before loading the user if the intersection is empty. A corrected implementation looks like this:
// FIXED — ApiKeyAuthenticator.php (>= 1.0.6)
class ApiKeyAuthenticator
{
public function authenticate(Request $request): ?User
{
$apiKey = $request->headers->get('X-API-Key');
if (!$apiKey) {
return null;
}
foreach ($this->keyStore->all() as $keyRecord) {
if (!hash_equals($keyRecord['key'], $apiKey)) {
continue;
}
// Enforce scope BEFORE loading the user
$requiredScope = $this->scopeResolver->resolveForRequest($request);
$grantedScopes = $keyRecord['scopes'] ?? [];
if (!in_array($requiredScope, $grantedScopes, true)) {
// Key exists but does not have the required scope — deny
return null;
}
return $this->userFactory->load($keyRecord['owner']);
}
return null;
}
}
Beyond upgrading, teams should:
- Rotate all existing API keys after upgrading, as there is no way to determine whether a key was used outside its declared scope prior to patching.
- Audit key ownership: keys owned by administrator accounts pose the greatest risk; re-issue them under least-privilege user accounts where possible.
- Review access logs for API calls that contradict the declared scope of the key used, treating anomalies as potential indicators of exploitation.
Our Take
This vulnerability is an instance of a pattern we see repeatedly across API-centric frameworks: the data contract and the enforcement contract are maintained in different places, and only one of them is implemented. The scopes field was presumably designed with the right intent — and even persisted correctly — but the corresponding check in the authentication layer was never written. Without automated enforcement testing (either through unit tests that exercise scope denial, or through DAST probing of scope boundaries), this gap is invisible to code reviewers focused on the happy path.
For enterprises operating headless CMS platforms or any system where API keys are distributed to third parties, this class of flaw is particularly dangerous because it silently violates the trust model on which access delegation is built. A vendor assures an integrator: “this key is read-only.” The integrator designs their system, and potentially their threat model, around that guarantee. When the guarantee is fictitious, the resulting exposure is often invisible until exploitation occurs.
Developers should treat scope enforcement as a first-class requirement with mandatory test coverage: for every scope level defined, a test must assert that a key bearing only that scope is rejected when it attempts an out-of-scope operation.
Detection with SAST
SAST detection for this pattern targets the gap between credential loading and scope enforcement. Offensive360’s analysis engine flags the following patterns in PHP API authentication code:
- Unsanitized credential-to-principal resolution: any code path where an API key, token, or credential is validated by equality and immediately followed by a full user/account object load, with no intervening scope or permission check.
- Dead field detection: structured data (arrays, objects) that contains fields semantically associated with access control (
scope,permissions,roles,grants) that are never read after construction or deserialization. - Missing scope intersection: authentication handlers that accept a
scopesparameter at key creation but lack any call to a scope comparison or intersection function before returning a principal.
This vulnerability falls under CWE-862 (Missing Authorization) and CWE-285 (Improper Authorization). In Offensive360’s ruleset, it is categorized under the AUTH-SCOPE-BYPASS rule family, which specifically models the pattern of declared-but-unenforced access constraints in API authentication middleware.
References
Detect this vulnerability class in your codebase
Offensive360 SAST scans your source code for CVE-2026-62231-class vulnerabilities and thousands of other patterns — across 60+ languages.