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. The token guard reads a client_credentials token's subject claim as a user identifier. That can resolve an unrelated real user and give 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. Laravel runs 77.5 percent of PHP-framework-powered websites, so the potential blast radius is large for organizations that rely on Passport for API authentication.
Technical information
Root cause: semantic mismatch in JWT subject handling
The vulnerability comes from a disconnect between two layers of the authentication stack. Laravel Passport depends on the League OAuth2 server library (league/oauth2-server), which handles the actual token issuance. A client_credentials grant has no user in the flow, and the token represents a machine rather than a person. For these grants, the League library sets the JWT sub (subject) claim to the client identifier instead of 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 guard never checks whether the sub value is a user identifier or a client identifier. If the database holds a user record whose primary key matches the client identifier value, the guard resolves that user and treats the request as fully authenticated on that user's behalf.
Configuration-dependent exploitability
Whether an attacker can exploit this in practice depends heavily on how the application configures client identifiers.
Default configuration (UUID client IDs): Laravel Passport uses UUIDs for client identifiers by default. User primary keys are typically auto-incrementing integers, so 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. Here, any client_credentials token issued to a client with ID 1 resolves as User ID 1. User ID 1 is often the first administrator account created during application setup, which makes this 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 sends client_credentials tokens through the guard's 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 whosesubclaim 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 every resource and permission belonging to the resolved user, without ever holding that user's credentials.
Every token involved is cryptographically valid. The server issued it, the signature checks out, and nobody has tampered with it. The problem lies 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 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 responded to this vulnerability quickly and in coordination with others. The issue was reported on GitHub (Issue #1900), and two pull requests (#1901 and #1902) were submitted to address the flaw. A GitHub Security Advisory (GHSA-349c-2h2f-mxf6) followed, and the fix shipped in version 13.7.1. The Laravel Passport maintainers worked with the upstream league/oauth2-server project throughout, which reflects a mature 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



