mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-03 21:42:50 +08:00
- api_keys table + hashed key store (src/data/api-keys.ts), tsmb_-prefixed plaintext shown once, per-user cap of 20, lastUsedAt tracking - requireAuth accepts Authorization: Bearer / X-API-Key headers as an alternative to the session cookie; key inherits the owner user's role/capabilities/bot scope - csrf origin check skipped for key-only requests (no ambient credentials); requests that also carry the session cookie stay gated - /api/keys management endpoints (session-only, guests excluded, keys themselves rejected) with audit logging - user deletion / password reset cascade-revoke the user's keys - Settings page: API key management section (create/copy-once/revoke) - docs: README section + full endpoint reference in docs/API.md
98 lines
3.5 KiB
TypeScript
98 lines
3.5 KiB
TypeScript
import { describe, it, expect, beforeEach } from "vitest";
|
|
import express from "express";
|
|
import request from "supertest";
|
|
import { csrfOriginCheck } from "./csrf.js";
|
|
|
|
describe("csrfOriginCheck middleware", () => {
|
|
let app: express.Express;
|
|
|
|
beforeEach(() => {
|
|
app = express();
|
|
app.use(csrfOriginCheck);
|
|
app.get("/", (_req, res) => res.json({ ok: true }));
|
|
app.post("/", (_req, res) => res.json({ ok: true }));
|
|
});
|
|
|
|
it("allows safe methods (GET/HEAD/OPTIONS) without Origin", async () => {
|
|
const res = await request(app).get("/");
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
it("rejects POST without Origin or Referer", async () => {
|
|
const res = await request(app).post("/");
|
|
expect(res.status).toBe(403);
|
|
expect(res.body).toEqual({ error: "bad origin" });
|
|
});
|
|
|
|
it("accepts POST when Origin host matches request host", async () => {
|
|
const res = await request(app)
|
|
.post("/")
|
|
.set("Host", "example.com")
|
|
.set("Origin", "https://example.com");
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
it("rejects POST when Origin host does not match request host", async () => {
|
|
const res = await request(app)
|
|
.post("/")
|
|
.set("Host", "example.com")
|
|
.set("Origin", "https://evil.com");
|
|
expect(res.status).toBe(403);
|
|
});
|
|
|
|
it("accepts POST when Referer host matches and Origin is absent", async () => {
|
|
const res = await request(app)
|
|
.post("/")
|
|
.set("Host", "example.com")
|
|
.set("Referer", "https://example.com/some/path");
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
it("rejects POST when Referer host does not match", async () => {
|
|
const res = await request(app)
|
|
.post("/")
|
|
.set("Host", "example.com")
|
|
.set("Referer", "https://evil.com/some/path");
|
|
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" });
|
|
});
|
|
|
|
// API-key clients authenticate via a header the browser never attaches
|
|
// automatically, so CSRF cannot abuse them — the origin check is skipped.
|
|
it("allows POST with an X-API-Key header and no session cookie", async () => {
|
|
const res = await request(app).post("/").set("X-API-Key", "tsmb_abc");
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
it("allows POST with an Authorization: Bearer key and no session cookie", async () => {
|
|
const res = await request(app).post("/").set("Authorization", "Bearer tsmb_abc");
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
it("does NOT skip the origin check when a session cookie rides along with an API key", async () => {
|
|
// An attacker page can set arbitrary headers while the victim's cookie is
|
|
// attached ambiently — the cookie keeps the request under the gate.
|
|
const res = await request(app)
|
|
.post("/")
|
|
.set("Host", "example.com")
|
|
.set("Origin", "https://evil.com")
|
|
.set("Cookie", "tsmb_session=whatever")
|
|
.set("X-API-Key", "tsmb_abc");
|
|
expect(res.status).toBe(403);
|
|
});
|
|
});
|