diff --git a/.env.example b/.env.example index 32073d9..f838443 100644 --- a/.env.example +++ b/.env.example @@ -3,9 +3,6 @@ TALLYNOTE_PORT=3000 TALLYNOTE_DATA_DIR=./data TALLYNOTE_TIMEZONE=Asia/Shanghai TALLYNOTE_PUBLIC_ORIGIN=http://127.0.0.1:3000 -# Optional additional browser Origins for an explicit reverse-proxy alias. -# Keep the primary public origin above and list only trusted HTTPS origins. -# TALLYNOTE_ALLOWED_ORIGINS=https://tally.example.com,https://tally.internal.example TALLYNOTE_TRUST_PROXY=false TALLYNOTE_COOKIE_SECURE=false # Set TALLYNOTE_HOST=0.0.0.0 and the server's real IP Origin for direct diff --git a/README.md b/README.md index 7f5367f..4934c1c 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ curl --proto '=https' --tlsv1.2 -fsSL https://git.awaioi.com/awaioi/TallyNote/ra 卸载器会逐项输出停止、禁用和删除进度;每次 systemd/dbus 调用默认最多等待 30 秒,避免终端无限无响应。可通过 `TALLYNOTE_UNINSTALL_SYSTEMCTL_TIMEOUT_SECONDS` 调整超时时间。 -公网反代必须使用 HTTPS,并在环境文件中设置真实的 `TALLYNOTE_PUBLIC_ORIGIN=https://...`、`TALLYNOTE_COOKIE_SECURE=true` 和明确的 `TALLYNOTE_TRUST_PROXY` 跳数(不要使用生产值 `true`)。登录和所有写入请求会校验浏览器 `Origin`;反代必须原样转发 `Origin`,且访问地址必须与 `TALLYNOTE_PUBLIC_ORIGIN` 完全一致。若确实需要多个受信任域名,可用 `TALLYNOTE_ALLOWED_ORIGINS=https://a.example.com,https://b.example.com` 显式列出(只写 Origin,不含路径),不要把它设为任意来源。 +公网反代推荐使用 HTTPS,并在环境文件中设置真实的 `TALLYNOTE_PUBLIC_ORIGIN=https://...`、`TALLYNOTE_COOKIE_SECURE=true` 和明确的 `TALLYNOTE_TRUST_PROXY` 跳数(不要使用生产值 `true`)。反代只需把域名转发到 TallyNote 端口并保留 `Host`、`X-Forwarded-Proto`;应用不会因为代理缺少或改写浏览器 `Origin` 而拦截登录。已认证写请求仍使用会话 Cookie 与 CSRF 令牌保护。 ### 构建发布包 diff --git a/install.sh b/install.sh index bbf0632..ed0e27c 100755 --- a/install.sh +++ b/install.sh @@ -1460,7 +1460,6 @@ main() { elif [[ -n "${TALLYNOTE_PUBLIC_ORIGIN+x}" ]]; then set_env_key TALLYNOTE_PUBLIC_ORIGIN "$INSTALL_PUBLIC_ORIGIN" fi - if [[ -n "${TALLYNOTE_ALLOWED_ORIGINS+x}" ]]; then set_env_key TALLYNOTE_ALLOWED_ORIGINS "$TALLYNOTE_ALLOWED_ORIGINS"; fi if [[ -n "${TALLYNOTE_ALLOW_INSECURE_HTTP+x}" ]]; then set_env_key TALLYNOTE_ALLOW_INSECURE_HTTP "$INSTALL_ALLOW_INSECURE_HTTP"; fi ensure_env_key TALLYNOTE_INSTALL_PREFIX "$PREFIX" ensure_env_key TALLYNOTE_DATA_DIR "$DATA_DIR" diff --git a/package.json b/package.json index 282e5fa..6108fb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tallynote", - "version": "1.1.21", + "version": "1.1.22", "private": true, "type": "module", "packageManager": "pnpm@9.0.6", diff --git a/server/app.ts b/server/app.ts index d138ec0..b91c3f5 100644 --- a/server/app.ts +++ b/server/app.ts @@ -648,19 +648,6 @@ export async function buildApp(database: DatabaseContext, config: AppConfig) { return payload; }); - app.addHook("onRequest", async (request) => { - if (!unsafeMethods.has(request.method) || !request.url.startsWith("/api/")) return; - const origin = request.headers.origin; - const allowed = new Set(config.allowedOrigins); - if (!config.isProduction) { - allowed.add("http://127.0.0.1:5173"); - allowed.add("http://localhost:5173"); - } - if (typeof origin !== "string" || !allowed.has(origin)) { - throw new AppError(403, "ORIGIN_FORBIDDEN", "请求来源不受信任"); - } - }); - app.setErrorHandler((error, request, reply) => { if (error instanceof AppError) return reply.code(error.statusCode).send(errorPayload(request, error)); if (error instanceof ZodError) { diff --git a/server/config.ts b/server/config.ts index a7ff47a..4440cb9 100644 --- a/server/config.ts +++ b/server/config.ts @@ -43,21 +43,6 @@ function csvEnv(name: string): string[] { .filter(Boolean); } -function originListEnv(name: string, primary: string): string[] { - const values = [primary, ...csvEnv(name)]; - const origins = new Set(); - for (const value of values) { - try { - const parsed = new URL(value); - if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.pathname !== "/" && parsed.pathname !== "" || parsed.search || parsed.hash) throw new Error(); - origins.add(parsed.origin); - } catch { - throw new Error(`${name} 必须是逗号分隔的 HTTP(S) Origin(不含路径)`); - } - } - return [...origins]; -} - function updatePublicKeyEnv(): string | undefined { const inline = process.env.TALLYNOTE_UPDATE_PUBLIC_KEY?.trim(); const file = process.env.TALLYNOTE_UPDATE_PUBLIC_KEY_FILE?.trim(); @@ -113,7 +98,6 @@ export function loadConfig() { throw new Error("TALLYNOTE_PUBLIC_ORIGIN 不能使用通配监听地址,请填写服务器 IP 或域名"); } const localOrigin = ["127.0.0.1", "localhost", "::1"].includes(publicHost); - const allowedOrigins = originListEnv("TALLYNOTE_ALLOWED_ORIGINS", parsedOrigin.origin); const appVersion = (() => { try { const packageJson = JSON.parse(readFileSync(path.join(projectRoot, "package.json"), "utf8")) as { version?: unknown }; @@ -143,7 +127,6 @@ export function loadConfig() { host, port, publicOrigin: parsedOrigin.origin, - allowedOrigins, timezone, trustProxy: trustProxyEnv(), cookieSecure, diff --git a/systemd/tallynote.env.example b/systemd/tallynote.env.example index ee64d19..1cc6d8c 100644 --- a/systemd/tallynote.env.example +++ b/systemd/tallynote.env.example @@ -3,7 +3,6 @@ TALLYNOTE_PORT=3000 TALLYNOTE_DATA_DIR=/var/lib/tallynote TALLYNOTE_INSTALL_PREFIX=/opt/tallynote TALLYNOTE_PUBLIC_ORIGIN=http://127.0.0.1:3000 -TALLYNOTE_ALLOWED_ORIGINS=http://127.0.0.1:3000 TALLYNOTE_COOKIE_SECURE=false TALLYNOTE_ALLOW_INSECURE_HTTP=false TALLYNOTE_TIMEZONE=Asia/Shanghai diff --git a/tests/api.test.ts b/tests/api.test.ts index af10956..9625ea9 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -129,10 +129,10 @@ describe("TallyNote API", () => { } }); - it("拒绝没有 Origin 的写请求", async () => { + it("反向代理缺少 Origin 时仍允许登录请求进入认证流程", async () => { const response = await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "x", password: "x" } }); - expect(response.statusCode).toBe(403); - expect(response.json().error.code).toBe("ORIGIN_FORBIDDEN"); + expect(response.statusCode).toBe(401); + expect(response.json().error.code).toBe("INVALID_CREDENTIALS"); }); it("将非法 JSON、伪造请求 ID 处理为结构化 400", async () => { diff --git a/tests/security.test.ts b/tests/security.test.ts index eadf624..5cc6f73 100644 --- a/tests/security.test.ts +++ b/tests/security.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { loadConfig, prepareDataDirectories } from "../server/config.js"; -const keys = ["TALLYNOTE_DATA_DIR", "TALLYNOTE_HOST", "TALLYNOTE_PORT", "TALLYNOTE_PUBLIC_ORIGIN", "TALLYNOTE_ALLOWED_ORIGINS", "TALLYNOTE_COOKIE_SECURE", "TALLYNOTE_ALLOW_INSECURE_HTTP", "TALLYNOTE_TRUST_PROXY", "NODE_ENV", "TALLYNOTE_ENV", "TALLYNOTE_UPDATE_STRATEGY", "TALLYNOTE_UPDATE_ALLOWED_HOSTS", "TALLYNOTE_UPDATE_REQUIRE_SIGNATURE", "TALLYNOTE_UPDATE_PUBLIC_KEY", "TALLYNOTE_UPDATE_PUBLIC_KEY_FILE"]; +const keys = ["TALLYNOTE_DATA_DIR", "TALLYNOTE_HOST", "TALLYNOTE_PORT", "TALLYNOTE_PUBLIC_ORIGIN", "TALLYNOTE_COOKIE_SECURE", "TALLYNOTE_ALLOW_INSECURE_HTTP", "TALLYNOTE_TRUST_PROXY", "NODE_ENV", "TALLYNOTE_ENV", "TALLYNOTE_UPDATE_STRATEGY", "TALLYNOTE_UPDATE_ALLOWED_HOSTS", "TALLYNOTE_UPDATE_REQUIRE_SIGNATURE", "TALLYNOTE_UPDATE_PUBLIC_KEY", "TALLYNOTE_UPDATE_PUBLIC_KEY_FILE"]; afterEach(() => { for (const key of keys) delete process.env[key]; }); @@ -48,14 +48,6 @@ describe("部署安全配置", () => { expect(loadConfig().trustProxy).toBe(1); }); - it("允许显式列出反向代理的多个可信 Origin", () => { - process.env.TALLYNOTE_PUBLIC_ORIGIN = "https://tally.example.test"; - process.env.TALLYNOTE_COOKIE_SECURE = "true"; - process.env.TALLYNOTE_ALLOWED_ORIGINS = "https://tally.example.test, https://tally.internal.test:8443"; - expect(loadConfig().allowedOrigins).toEqual(["https://tally.example.test", "https://tally.internal.test:8443"]); - process.env.TALLYNOTE_ALLOWED_ORIGINS = "https://tally.example.test/app"; - expect(() => loadConfig()).toThrow(/Origin/); - }); it("systemd 更新必须绑定主机白名单,签名校验默认关闭", () => { process.env.TALLYNOTE_UPDATE_STRATEGY = "systemd";