a login through the proxy lost its csrf cookie, and looped

`stripHopByHop` copies headers with `out.set(key, value)`, which overwrites.
Correct for every header except the one that legitimately repeats: a NextAuth
sign-in sends several Set-Cookie - csrf-token, callback-url, session-token - and
collapsing them to one drops the rest.

Measured: two Set-Cookie headers direct from Linkwarden, one through the door,
and the survivor was callback-url. So the csrf cookie never reached the browser,
the sign-in posted, failed its CSRF check and bounced back to the form. A
redirect loop with nothing in any log saying why - and the owner predicted this
exact symptom from the other side before it was found.

`getSetCookie()` returns them unjoined, which is the reason to use it rather than
splitting a combined value: `Expires` contains a comma, so naive splitting
corrupts the date and the cookie is discarded.

Both hops use this one function, so both are fixed by it.
This commit is contained in:
2026-09-05 21:23:48 +01:00
parent 05d51302ec
commit f7c9a28c33
2 changed files with 20 additions and 1 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ import type { PluginManifest } from '@@/plugins/manifest';
*/
export const manifest: PluginManifest = {
publisher: 'officerdev',
version: '2026.09.05.04',
version: '2026.09.05.05',
platform: '>=1.0.0',
label: 'Linkwarden',
+19
View File
@@ -99,6 +99,25 @@ export function stripResponseHeaders(src: Headers): Headers {
const out = stripHopByHop(src);
out.delete('content-encoding');
out.delete('content-length');
/*
── `Set-Cookie` is the one header that legitimately repeats, and `.set()` keeps only the last ──
`stripHopByHop` copies with `out.set(key, value)`, which OVERWRITES. Everything else is fine that
way; Set-Cookie is not. A NextAuth sign-in sends several — csrf-token, callback-url, session-token —
and collapsing them to one silently drops the rest.
Measured on this install: two headers direct from Linkwarden, one through the proxy, and the one
that survived was `callback-url`. The csrf cookie never reached the browser, which is a sign-in that
posts, fails its CSRF check, and bounces back to the form — the redirect loop, with nothing in any
log to say why.
`getSetCookie()` is the standard accessor for exactly this case: it returns the values unjoined,
which matters because a cookie's `Expires` contains a comma and naive comma-splitting corrupts it.
*/
out.delete('set-cookie');
for (const cookie of src.getSetCookie()) out.append('set-cookie', cookie);
return out;
}