Merge pull request #77 from ZHANGTIANYAO1/fix/webui-referrer-policy-csrf

fix(web): referrer-policy same-origin 修复扫码登录不弹二维码 + cookie 无法保存
This commit is contained in:
TIANYAO ZHANG authored and GitHub committed 2026-05-29 21:30:35 +08:00
commit 0401534b88
3 files changed
+73 -4

No files matched your search

+15
View File
@@ -55,4 +55,19 @@ describe("csrfOriginCheck middleware", () => {
.set("Referer", "https://evil.com/some/path"); .set("Referer", "https://evil.com/some/path");
expect(res.status).toBe(403); expect(res.status).toBe(403);
}); });
// Documents the server side of the QR-login outage: a `no-referrer` document
// policy makes the browser send the literal `Origin: null` on same-origin
// POSTs, which this guard cannot parse a host from and therefore rejects.
// The fix lives in the frontend (referrer policy -> same-origin); this test
// pins the gate behavior so the interaction stays understood. See
// src/web/referrer-policy.test.ts.
it('rejects POST with the literal Origin: "null" (no-referrer downgrade)', async () => {
const res = await request(app)
.post("/")
.set("Host", "example.com")
.set("Origin", "null");
expect(res.status).toBe(403);
expect(res.body).toEqual({ error: "bad origin" });
});
}); });
+46
View File
@@ -0,0 +1,46 @@
import { describe, it, expect } from "vitest";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
/**
* Regression guard for the QR-login / cookie-save outage (and in fact every
* mutating WebUI action). On 2026-05-27 the WebUI-auth feature added the
* same-origin CSRF gate `app.use("/api", csrfOriginCheck)` in server.ts, and
* the same day a `<meta name="referrer" content="no-referrer">` was added to
* web/index.html so cross-origin CDN cover thumbnails would load.
*
* Those two changes conflict: per the WHATWG Fetch "Append a request Origin
* header" algorithm, the `no-referrer` policy sets the Origin header to the
* literal string "null" on same-origin non-GET requests. csrfOriginCheck then
* fails to parse a host (`new URL("null")` throws) and returns 403 "bad
* origin", so POST /api/auth/qrcode (and every other POST/PUT/DELETE under
* /api/* except /api/session/*) never reaches its handler.
*
* `same-origin` is the correct policy: it keeps the real Origin on same-origin
* requests (CSRF passes) while still sending no Referer cross-origin (CDN
* thumbnails keep loading). Never switch this back to `no-referrer`.
*/
describe("frontend referrer policy (CSRF / Origin-header regression)", () => {
const indexHtmlPath = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../../web/index.html"
);
const html = fs.readFileSync(indexHtmlPath, "utf-8");
const referrerMeta = html.match(
/<meta\s+name=["']referrer["']\s+content=["']([^"']+)["']\s*\/?>/i
);
it("declares a referrer policy meta tag", () => {
expect(referrerMeta).not.toBeNull();
});
it("uses same-origin (NOT no-referrer, which sends Origin: null and 403s every POST)", () => {
expect(referrerMeta?.[1]).toBe("same-origin");
});
it("does not contain no-referrer anywhere in the document head", () => {
expect(html).not.toMatch(/content=["']no-referrer["']/i);
});
});
+12 -4
View File
@@ -3,10 +3,18 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Bilibili / NetEase / QQ image CDNs reject requests whose Referer is not on their whitelist. <!-- Bilibili / NetEase / QQ image CDNs reject requests whose Referer is not on
Setting no-referrer at the document level covers <img> tags AND CSS background-image fetches. their whitelist, so we must not leak a Referer cross-origin. "same-origin"
Our own /api/* CSRF check uses Origin (not Referer), so this doesn't break auth. --> does exactly that: full Referer for our own requests, none for cross-origin
<meta name="referrer" content="no-referrer"> ones — so cover thumbnails (<img> AND CSS background-image) still load.
Do NOT switch this back to "no-referrer": per the WHATWG Fetch spec
("Append a request Origin header") no-referrer downgrades the Origin header
to the literal string "null" on same-origin non-GET requests. The /api/*
CSRF guard (src/web/middleware/csrf.ts) then can't parse a host from it and
responds 403 "bad origin", silently breaking EVERY POST/PUT/DELETE — QR
login, cookie save, playback controls, bot management, user admin, etc.
"same-origin" keeps the real Origin on same-origin requests, so CSRF passes. -->
<meta name="referrer" content="same-origin">
<title>TSMusicBot</title> <title>TSMusicBot</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;500;600;700;800&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;500;600;700;800&display=swap" rel="stylesheet">