payloadcms / payloadcms/payload
Default `unlock` access lets any authenticated user unlock any other account (CVE-2026-11779) — and the access function receives no `data`
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 44.8k
- Forks
- 4.2k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 53
Description
Describe the Bug
What happens
On an auth-enabled collection, access.unlock defaults to defaultAccess, i.e. ({ req: { user } }) => Boolean(user) (packages/payload/src/collections/config/defaults.ts, packages/payload/src/auth/defaultAccess.ts). So any authenticated user of any auth collection — a low-privilege editor, for instance — can call POST /api/<auth-collection>/unlock with someone else's email and reset that account's loginAttempts / lockUntil. The brute-force protection (maxLoginAttempts / lockTime) can therefore be defeated by an attacker who holds any valid account: lock-outs on the accounts they are guessing can be cleared at will.
This is the behaviour described by CVE-2026-11779 / GHSA-jg8r-5jh2-v2xj (published 2026-06-26, "unreviewed", no patched version listed). I could not find an issue or a PR tracking it, so here it is. Checked on the installed payload@3.88.0 (latest at the time of writing) and on main.
A second, related limitation makes it hard to write a safe rule oneself: unlockOperation calls executeAccess({ slug, req }, collectionConfig.access.unlock) (packages/payload/src/auth/operations/unlock.ts) — the access function never receives data (the targeted email / username). So a rule like "a user may only unlock themselves" or "only accounts in my team" cannot be expressed; the only workable rule is role-based or () => false. This is also what #14459 is about.
Proposal (happy to open the PR):
- Change the default for
unlock: it should not beBoolean(user). The safest default is to deny unless configured (() => false) — or, less strict, a documented default that requires an explicit opt-in. Since this changes behaviour, I understand it targets v4; a v3 patch could keep the default but log a warning whenunlockis left unconfigured. - Pass
data(the sanitizedemail/usernamebeing unlocked) to theunlockaccess function, like other operations passdata, so per-target rules become possible.
Workaround for existing projects (what we did): set access.unlock explicitly on every auth collection, e.g. restricted to an admin role.
I would appreciate a word from the maintainers on the preferred target (3.x patch vs v4 default change) before opening the PR.
Link to the code that reproduces this issue
Reproduction Steps
No custom code is needed: the behaviour comes from the defaults linked above. On a blank project (pnpx create-payload-app@latest -t blank, the default users auth collection, no access configured):
- Create three users:
admin@example.com,victim@example.com,attacker@example.com(any roles — the collection has none by default). - Lock
victimby failing the loginmaxLoginAttemptstimes (default 5):POST /api/users/loginwith a wrong password until the response says the account is locked. In the database,loginAttempts= 5 andlockUntilis set. - Log in as
attacker(POST /api/users/login) and keep thepayload-tokencookie. - With that cookie, call
POST /api/users/unlockwith body{ "email": "victim@example.com" }.
Expected: the request is refused — attacker has no business unlocking another account.
Actual: the access check passes (Boolean(req.user) is true for any logged-in user), resetLoginAttempts runs, and victim's loginAttempts / lockUntil are reset — the brute-force protection is gone. Reading of unlockOperation: the only guard before the reset is executeAccess({ slug, req }, collectionConfig.access.unlock).
Same result with any authenticated user of any auth collection.
Second point (no data passed to the access function): set access.unlock: ({ req, data }) => data?.email === req.user?.email on the collection — data is undefined in the unlock access call, so the rule cannot be expressed (see #14459).
Which area(s) are affected?
area: core
Environment Info
payload: 3.88.0 (defaults unchanged on main)
@payloadcms/db-postgres: 3.88.0
@payloadcms/next: 3.88.0
next: 16.3.1
node: 22.x
OS: Linux (Docker) / Windows 11 dev
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with packages/payload/src/collections/config/defaults.ts, packages/payload/src/auth/defaultAccess.ts, and packages/payload/src/auth/operations/unlock.ts to trace the default access check and its arguments. Reproduce the behavior with the documented login and POST /api/users/unlock steps, then verify the chosen version/default policy prevents unauthorized unlocks and makes the sanitized target data available to the access function.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- authentication, backend, security
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100