payloadcms / payloadcms/payload

Cookie auth rejects same-origin requests over plain HTTP (regression in 3.79.1)

Open
#17,565 2 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area: core status: needs-triage v3
Dominant language
TypeScript
Stars
44.8k
Forks
4.2k
Avg merge
2d 21h
Merged PRs (30d)
53

Description

Describe the Bug

Since 3.79.1 a valid payload-token cookie is discarded when the request carries neither Origin nor Sec-Fetch-Site. That is what a browser sends for a same-origin navigation over plain HTTP: Origin is omitted for navigations, and Sec-Fetch-* goes only to potentially trustworthy origins] (see W3 spec), which http:// on a non-localhost host is not.

Effect: login returns 200 and sets the cookie, /admin bounces straight back to /admin/login.

Local development is commonly served over plain HTTP under a hostname rather than localhost, which is where this bites — Docksal, DDEV, Lando, or a plain reverse proxy in front of the dev server.

What changed in dist/auth/extractJWT.js, cookie strategy. 3.79.0 accepted a
missing Origin:

if (!origin || payload.config.csrf.length === 0 || payload.config.csrf.indexOf(origin) > -1) {
  return cookieToken
}

3.79.1 requires Sec-Fetch-Site instead, and returns null without it.

csrf: [] is not a way out: dist/config/sanitize.js does config.csrf.push(config.serverURL), so the csrf.length === 0 branch is unreachable
for anyone who sets serverURL.

Link to the code that reproduces this issue

https://github.com/fullheart/payload-issue-17565

Reproduction Steps

See README: https://github.com/fullheart/payload-issue-17565/blob/main/README.md#run-it

Which area(s) are affected?

area: core

Environment Info
Binaries:
  Node: 24.18.1
  npm: 11.16.0
Relevant Packages:
  payload: 3.86.0
  next: 16.2.12
  @payloadcms/db-postgres: 3.86.0
  react: 19.2.8
  react-dom: 19.2.8
Operating System:
  Platform: linux
  Arch: x64

Workaround

Via patch-package — treat an absent Sec-Fetch-Site as "the client did not say" rather than "cross-site", and fall back to the Referer origin, still checked against the same csrf allowlist:

         if (secFetchSite === 'same-origin' || secFetchSite === 'same-site' || secFetchSite === 'none') {
             return cookieToken;
         }
+        if (!secFetchSite) {
+            const referer = headers.get('Referer');
+            if (referer) {
+                try {
+                    if (payload.config.csrf.includes(new URL(referer).origin)) {
+                        return cookieToken;
+                    }
+                } catch {}
+            }
+        }
         // Reject cross-site requests and missing header (non-browser clients)
         return null;

UPDATE: Is this related #16031?

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in dist/auth/extractJWT.js and dist/config/sanitize.js, then run the README reproduction from payload-issue-17565. Verify that a valid cookie works for same-origin plain HTTP requests without Origin or Sec-Fetch-Site while the configured CSRF allowlist still rejects cross-site requests; add or update coverage for the regression if the repository’s relevant tests are located.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
authentication, backend, security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.