fix: lock the desktop backend to the app's own origin (GHSA-frx8-qqgg-7j7g)

This commit is contained in:
LukeGus committed 2026-10-07 21:14:34 -05:00
1 parent cd29e1d087
commit ad3a18224f
3 files changed
+64 -5

No files matched your search

+6 -1
View File
@@ -11,6 +11,7 @@ import { authLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js";
import { parseUserAgent } from "../../utils/user-agent-parser.js";
import { isDesktopAppOrigin } from "../../utils/cors-config.js";
import { deleteUserAndRelatedData } from "./delete-user-data.js";
import {
allowsDesktopAutoSession,
@@ -744,7 +745,11 @@ router.get("/setup-required", async (req, res) => {
*/
router.post("/internal/auto-session", async (req, res) => {
try {
if (!allowsDesktopAutoSession() || !isLoopbackRequest(req)) {
if (
!allowsDesktopAutoSession() ||
!isLoopbackRequest(req) ||
!isDesktopAppOrigin(req.headers.origin)
) {
authLogger.warn(
"Rejected non-loopback attempt to access auto-session endpoint",
{ source: req.ip },
+42 -1
View File
@@ -1,6 +1,9 @@
import { afterEach, describe, expect, it } from "vitest";
import type { Request } from "express";
import { isCorsOriginAllowed } from "../../utils/cors-config.js";
import {
isCorsOriginAllowed,
isDesktopAppOrigin,
} from "../../utils/cors-config.js";
function request(headers: Record<string, string> = {}): Request {
return {
@@ -11,6 +14,7 @@ function request(headers: Record<string, string> = {}): Request {
afterEach(() => {
delete process.env.CORS_ALLOWED_ORIGINS;
delete process.env.ELECTRON_EMBEDDED;
});
describe("isCorsOriginAllowed", () => {
@@ -41,4 +45,41 @@ describe("isCorsOriginAllowed", () => {
const req = request({ host: "termix.example" });
expect(isCorsOriginAllowed(req, "https://attacker.example")).toBe(false);
});
describe("embedded desktop backend", () => {
it("rejects other websites even with no allowlist", () => {
process.env.ELECTRON_EMBEDDED = "true";
const req = request({ host: "127.0.0.1:30001" });
expect(isCorsOriginAllowed(req, "https://evil.example")).toBe(false);
});
it("still allows the desktop app and the dev server", () => {
process.env.ELECTRON_EMBEDDED = "true";
const req = request({ host: "127.0.0.1:30001" });
expect(isCorsOriginAllowed(req, "file://")).toBe(true);
expect(isCorsOriginAllowed(req, "http://localhost:5173")).toBe(true);
expect(isCorsOriginAllowed(req, undefined)).toBe(true);
});
it("allows an explicitly configured origin", () => {
process.env.ELECTRON_EMBEDDED = "true";
process.env.CORS_ALLOWED_ORIGINS = "https://portal.example";
expect(isCorsOriginAllowed(request(), "https://portal.example")).toBe(
true,
);
});
});
});
describe("isDesktopAppOrigin", () => {
it("accepts no origin, file:// and the dev server", () => {
expect(isDesktopAppOrigin(undefined)).toBe(true);
expect(isDesktopAppOrigin("file://")).toBe(true);
expect(isDesktopAppOrigin("http://127.0.0.1:5173")).toBe(true);
});
it("rejects websites", () => {
expect(isDesktopAppOrigin("https://evil.example")).toBe(false);
expect(isDesktopAppOrigin("null")).toBe(false);
});
});
+16 -3
View File
@@ -14,15 +14,28 @@ function getAllowedOrigins(): string[] {
.filter(Boolean);
}
/**
* Origins allowed to drive the embedded desktop backend: the desktop app
* itself and the dev server. Any website open in a browser on the same
* machine can reach loopback too, so nothing else gets in.
*/
export function isDesktopAppOrigin(origin: string | undefined): boolean {
if (!origin) return true;
return (
DEV_ORIGINS.includes(origin) || origin.startsWith(ELECTRON_FILE_ORIGIN)
);
}
export function isCorsOriginAllowed(
req: Request,
origin: string | undefined,
): boolean {
if (!origin) return true;
if (DEV_ORIGINS.includes(origin)) return true;
if (origin.startsWith(ELECTRON_FILE_ORIGIN)) return true;
if (isDesktopAppOrigin(origin)) return true;
const configured = getAllowedOrigins();
if (process.env.ELECTRON_EMBEDDED === "true") {
return configured.includes(origin) || origin === getRequestOrigin(req);
}
if (configured.length === 0) return true;
if (configured.includes(origin)) return true;