Introduction
A machine to machine OAuth2 token should never grant access to a human user's account, but a flaw in Laravel Passport made exactly that possible. CVE-2026-39976 is an authentication bypass in Laravel Passport versions 13.0.0 through 13.7.0 where the token guard misinterprets a client_credentials token's subject claim as a user identifier, potentially resolving an unrelated real user and granting the token bearer that user's full privileges.
Laravel Passport is the official OAuth2 server implementation for the Laravel PHP framework, with over 85 million total downloads on Packagist. Given that 77.5 percent of PHP framework powered websites run Laravel, the potential blast radius of this vulnerability is significant for organizations relying on Passport for API authentication.
Technical Information
Root Cause: Semantic Mismatch in JWT Subject Handling
The vulnerability originates from a disconnect between two layers of the authentication stack. The League OAuth2 server library (league/oauth2-server), which Laravel Passport depends on, handles the actual token issuance. For client_credentials grants, there is no user involved in the flow; the token represents a machine, not a person. In this case, the League library sets the JWT sub (subject) claim to the client identifier rather than a user identifier.
Laravel Passport's TokenGuard::authenticateViaBearerToken() method then processes incoming bearer tokens. It extracts the sub claim and passes it directly to the user provider's retrieveById() method. The critical flaw is that the guard performs no validation to determine whether the sub value actually represents a user identifier or a client identifier. If a user record exists in the database whose primary key matches the client identifier value, the guard resolves that user and treats the request as fully authenticated on behalf of that user.
Configuration Dependent Exploitability
The practical exploitability of this vulnerability depends heavily on how client identifiers are configured within the application.
Default configuration (UUID client IDs): Laravel Passport uses UUIDs for client identifiers by default. Since user primary keys are typically auto incrementing integers, a UUID like 9a3b5c7d-1234-4e56-a789-0bcd1ef23456 will never match an integer user ID. In this configuration, the collision risk is extremely low and the vulnerability has minimal practical impact.
Integer client IDs: Applications that disable UUID client identifiers by setting Passport::clientUuids(false) switch to integer based client IDs. In this configuration, a client with ID 1 would cause any client_credentials token issued to that client to resolve as User ID 1. Since User ID 1 is often the first administrator account created during application setup, this represents a direct path to privilege escalation.
Improper middleware usage: Even with UUID client IDs, using the standard auth:api middleware instead of the dedicated client credentials middleware creates a situation where the guard logic processes client_credentials tokens through the user authentication path.
Attack Flow
- An attacker registers or obtains credentials for an OAuth2 client configured with the client_credentials grant type. This is a normal, expected operation.
- The attacker requests a token from the
/oauth/tokenendpoint using the client_credentials grant. The server issues a valid, properly signed JWT where thesubclaim contains the client's identifier. - The attacker presents this token to an API endpoint protected by the
auth:apimiddleware (rather than theEnsureClientIsResourceOwnermiddleware). - The
TokenGuardextracts thesubclaim from the JWT and callsretrieveById()on the configured user provider. - If a user exists whose primary key matches the client identifier (most likely in integer ID configurations), the guard authenticates the request as that user.
- The attacker now has full access to all resources and permissions belonging to the resolved user, without ever possessing the user's credentials.
The particularly insidious aspect of this vulnerability is that every token involved is cryptographically valid. The server issued it, the signature checks out, and the token has not been tampered with. The problem is purely in how the guard interprets the token's semantics.
Affected Systems and Versions
The vulnerability affects Laravel Passport versions 13.0.0 through 13.7.0 (all versions before 13.7.1).
The risk level varies by configuration:
| Configuration | Client ID Format | Collision Risk | Exploitation Potential |
|---|---|---|---|
| Default Setup | UUID | Extremely Low | Minimal |
Passport::clientUuids(false) | Integer | High | Critical |
Standard auth:api middleware on client routes | Any | Moderate | High |
Applications are most vulnerable when they combine integer based client IDs with the use of auth:api middleware on routes that accept client_credentials tokens.
The fix is available in Laravel Passport version 13.7.1.
Vendor Security History
Laravel's security team demonstrated a rapid and coordinated response to this vulnerability. The issue was reported via GitHub (Issue #1900), two pull requests (#1901 and #1902) were submitted to address the flaw, and a GitHub Security Advisory (GHSA-349c-2h2f-mxf6) was published. The fix was released in version 13.7.1. The coordination between the Laravel Passport maintainers and the upstream league/oauth2-server project reflects a mature vulnerability disclosure and remediation process.
References
- GitHub Security Advisory GHSA-349c-2h2f-mxf6
- Laravel Passport Issue #1900: TokenGuard does not validate oauth_user_id type
- Pull Request #1901: Prevent user impersonation via client credentials token
- Pull Request #1902: Fix resolve user as null when user ID matches client ID on integer key setups
- league/oauth2-server Issue #1456 Discussion
- Laravel Passport Official Documentation (13.x)
- laravel/passport on Packagist
- league/oauth2-server on Packagist
- League OAuth2 Server Database Setup Documentation



