Laravel Passport Can Authenticate a Machine Token as a Real User (CVE-2026-39976)
A client_credentials token is supposed to represent a machine, not a person. In Passport 13.0.0 through 13.7.0 it can end up authenticated as an unrelated user account. The conditions are specific, which is exactly why teams assume it does not apply to them.
There is a category of vulnerability that gets ignored because the preconditions read like they belong to somebody else. This is one of them, and the reason to read on even if you think it does not apply is that the condition it hinges on is a single boolean that plenty of teams changed years ago for a reason nobody remembers.
CVE-2026-39976 was published on 8 April 2026 as GHSA-349c-2h2f-mxf6. It carries a CVSS 3.1 score of 7.1, rated high, with the vector AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:N. It affects Laravel Passport 13.0.0 through 13.7.0 and is fixed in 13.7.1.
What goes wrong
OAuth2 has a grant type called client_credentials. It exists for machine-to-machine calls, where there is no user involved at all. A backend service authenticates as itself, gets a token, and calls your API.
Because no user is involved, the underlying league/oauth2-server library has to put something in the JWT's sub claim, and what it puts there is the client identifier.
Passport's TokenGuard reads that sub claim and passes it to retrieveById() on your user provider, without first checking whether the token represents a user at all. Your user provider does what it is told and looks up a user with that primary key.
If a user exists with a primary key equal to the client identifier, the guard returns that user. Your application now has a fully authenticated User model on a request that was made by a machine, and every downstream authorisation check, every policy, every auth()->user() call, sees a legitimate person who has no idea any of this is happening.
The condition that decides your exposure
The collision only occurs if client identifiers and user identifiers live in the same value space.
Passport defaults Passport::$clientUuids to true, which means clients are identified by UUIDs. A UUID never equals an integer primary key, so the lookup finds nothing and the collision route is closed. Most applications are fine for this reason, without having done anything deliberate about it.
The exposed configuration is Passport::$clientUuids = false, which gives clients auto-incrementing integer identifiers. Now client 1 and user 1 share a value space, and they collide from the very first row. The advisory specifically calls out the combination of integer client identifiers with the EnsureClientIsResourceOwner middleware as the high-risk case, which stings, because that middleware is exactly what people put on machine-to-machine routes.
Worth noting: the users who collide are your earliest ones. On most systems those are founders, staff, and administrators, because they signed up first. So the accounts most likely to be impersonated are also the ones with the most privilege.
Checking your application
Start with the version:
composer show laravel/passport | grep versions
Then find out whether you have the grant enabled at all. If client_credentials is not issued anywhere, the vulnerable path is unreachable:
grep -rn "client_credentials\|clientCredentials" app/ config/ routes/
Then the boolean that decides everything:
grep -rn "clientUuids" app/Providers/ config/
No result means you are on the default of true and the collision is closed. An explicit Passport::$clientUuids = false; means you have the risky configuration and should treat this as urgent rather than routine.
Finally, check whether the middleware appears on any route:
php artisan route:list | grep -i "client"
Fixing it
The upgrade is small and within a major version:
composer require laravel/passport:^13.7.1 --update-with-dependencies
php artisan config:clear
Then confirm it took, because a constraint elsewhere in your tree can silently pin you:
composer show laravel/passport | grep versions
composer audit
I would not attempt to remediate this by switching clientUuids to true on an existing installation instead of upgrading. That changes the identifier scheme for clients that already exist, which breaks every integration currently holding a client ID. Upgrade the package. If you also want UUIDs afterwards, treat that as a separate migration with its own rollout.
Looking for evidence
If you had the risky configuration, it is worth a look at your logs rather than assuming nothing happened. The signature is a request authenticated as a user, arriving on a route that should only ever see machine tokens, from the source address of a service rather than a browser.
Concretely, look for activity attributed to your lowest-numbered user accounts on API routes those people would never call, particularly outside working hours and without a plausible user agent. If you log the token or client identifier alongside the authenticated user, the mismatch is unmissable. If you do not log that pairing, this is a good moment to start, and the broader case for it is in security logging and monitoring failures in Laravel.
Absence of evidence in application logs is weak evidence here, since most applications do not log enough to distinguish this from ordinary traffic. Judge your actual risk on how long you ran an affected version with integer client IDs, not on whether you found something.
The wider lesson
The bug is a missing type check at a trust boundary. The sub claim means different things depending on the grant that produced the token, and the guard treated it as though it always meant the same thing.
That pattern is worth carrying into your own code. Anywhere an identifier crosses from one system into another, the receiving side should confirm it is the kind of thing it expects and not merely that a lookup succeeded. A successful lookup is not evidence of a correct one, and retrieveById() returning a User says nothing about whether a user was ever involved.
The same reasoning underpins how you should think about gates, policies, and the holes teams leave, where the recurring mistake is also assuming that the presence of an authenticated model means the authentication was meaningful.
StackShield flags known-vulnerable package versions in your production deployment along with the exposure paths that make them easier to reach. Run a free scan, or check the security checks reference for what we look at.
Is your Laravel app exposed right now?
34% of Laravel apps we scan have at least one critical issue. Most teams don't find out until something breaks. Our free scan checks your live application in under 60 seconds.
Frequently Asked Questions
What is CVE-2026-39976?
An authentication bypass in Laravel Passport, published 8 April 2026 as GHSA-349c-2h2f-mxf6 with a CVSS 3.1 score of 7.1, rated high. Passport's TokenGuard does not validate that the sub claim on a JWT belongs to a user before passing it to retrieveById. On client_credentials tokens the underlying OAuth2 server sets that claim to the client identifier, so a machine-to-machine token can end up authenticated as a user account whose primary key matches. Affected versions are 13.0.0 through 13.7.0, patched in 13.7.1.
Which Passport versions are affected?
Laravel Passport 13.0.0 through 13.7.0 inclusive. The fix is in 13.7.1. Run composer show laravel/passport to see what you are actually running. If you are on Passport 12 or earlier this advisory does not apply to you, though older major versions have their own support considerations worth reviewing separately.
Does this affect me if I use UUIDs for OAuth clients?
The collision route is closed if Passport::$clientUuids is left at its default of true, because a UUID cannot equal a user's integer primary key. That is the default, so most applications are protected from the specific collision scenario. The advisory still describes a general case where a machine token authenticates as a user, so upgrading remains the correct action rather than relying on your configuration to hold.
How do I check whether my application is exposed?
Three things: your Passport version, whether the client_credentials grant is enabled at all, and the value of Passport::$clientUuids in your service provider. If client_credentials is not enabled you are not exposed, since the vulnerable path requires that grant. If it is enabled and clientUuids is set to false, you have the collision condition and should treat the upgrade as urgent.
What is the EnsureClientIsResourceOwner middleware and why does it matter here?
It is Passport middleware that restricts a route to tokens issued through the client credentials grant, so it typically guards machine-to-machine endpoints. It matters here because the advisory calls out its combination with integer client identifiers as the specific high-risk configuration. Those are exactly the routes where an authenticated user object appearing unexpectedly is most likely to be trusted by downstream code.
Related Articles
Laravel Session Security: Cookies, Hijacking & config/session.php
A deep dive into Laravel session security. Learn how cookie flags, session drivers, and config/session.php settings protect against hijacking, fixation, and sidejacking attacks.
SecurityAutomated Security Testing in Laravel CI/CD Pipelines
How to add security gates to your Laravel CI/CD pipeline with GitHub Actions. Covers dependency scanning, static analysis, secret detection, and automated security monitoring.
SecurityLaravel Content Security Policy: Configure CSP Without Breaking Your App
Only 22% of Laravel apps have a Content Security Policy. Learn how to implement CSP with spatie/laravel-csp, handle Livewire and Vite nonces, and avoid the mistakes that break production.
Compare StackShield
Security Checklists
Laravel Production Deployment Security Checklist
A comprehensive security checklist for deploying Laravel applications to production. Covers environment config, server hardening, access control, and monitoring.
20 itemsLaravel API Security Checklist
Secure your Laravel API endpoints against common vulnerabilities. Covers authentication, input validation, rate limiting, and response security.
Stay Updated on Laravel Security
Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.