mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 23:55:23 +00:00
fix: lock the desktop backend to the app's own origin (GHSA-frx8-qqgg-7j7g)
This commit is contained in:
1 parent
cd29e1d087
commit
ad3a18224f
3 files changed
+64
-5
No files matched your search
@@ -11,6 +11,7 @@ import { authLogger } from "../../utils/logger.js";
|
|||||||
import { AuthManager } from "../../utils/auth-manager.js";
|
import { AuthManager } from "../../utils/auth-manager.js";
|
||||||
import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js";
|
import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js";
|
||||||
import { parseUserAgent } from "../../utils/user-agent-parser.js";
|
import { parseUserAgent } from "../../utils/user-agent-parser.js";
|
||||||
|
import { isDesktopAppOrigin } from "../../utils/cors-config.js";
|
||||||
import { deleteUserAndRelatedData } from "./delete-user-data.js";
|
import { deleteUserAndRelatedData } from "./delete-user-data.js";
|
||||||
import {
|
import {
|
||||||
allowsDesktopAutoSession,
|
allowsDesktopAutoSession,
|
||||||
@@ -744,7 +745,11 @@ router.get("/setup-required", async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
router.post("/internal/auto-session", async (req, res) => {
|
router.post("/internal/auto-session", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!allowsDesktopAutoSession() || !isLoopbackRequest(req)) {
|
if (
|
||||||
|
!allowsDesktopAutoSession() ||
|
||||||
|
!isLoopbackRequest(req) ||
|
||||||
|
!isDesktopAppOrigin(req.headers.origin)
|
||||||
|
) {
|
||||||
authLogger.warn(
|
authLogger.warn(
|
||||||
"Rejected non-loopback attempt to access auto-session endpoint",
|
"Rejected non-loopback attempt to access auto-session endpoint",
|
||||||
{ source: req.ip },
|
{ source: req.ip },
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import type { Request } from "express";
|
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 {
|
function request(headers: Record<string, string> = {}): Request {
|
||||||
return {
|
return {
|
||||||
@@ -11,6 +14,7 @@ function request(headers: Record<string, string> = {}): Request {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
delete process.env.CORS_ALLOWED_ORIGINS;
|
delete process.env.CORS_ALLOWED_ORIGINS;
|
||||||
|
delete process.env.ELECTRON_EMBEDDED;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("isCorsOriginAllowed", () => {
|
describe("isCorsOriginAllowed", () => {
|
||||||
@@ -41,4 +45,41 @@ describe("isCorsOriginAllowed", () => {
|
|||||||
const req = request({ host: "termix.example" });
|
const req = request({ host: "termix.example" });
|
||||||
expect(isCorsOriginAllowed(req, "https://attacker.example")).toBe(false);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -14,15 +14,28 @@ function getAllowedOrigins(): string[] {
|
|||||||
.filter(Boolean);
|
.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(
|
export function isCorsOriginAllowed(
|
||||||
req: Request,
|
req: Request,
|
||||||
origin: string | undefined,
|
origin: string | undefined,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!origin) return true;
|
if (isDesktopAppOrigin(origin)) return true;
|
||||||
if (DEV_ORIGINS.includes(origin)) return true;
|
|
||||||
if (origin.startsWith(ELECTRON_FILE_ORIGIN)) return true;
|
|
||||||
|
|
||||||
const configured = getAllowedOrigins();
|
const configured = getAllowedOrigins();
|
||||||
|
if (process.env.ELECTRON_EMBEDDED === "true") {
|
||||||
|
return configured.includes(origin) || origin === getRequestOrigin(req);
|
||||||
|
}
|
||||||
if (configured.length === 0) return true;
|
if (configured.length === 0) return true;
|
||||||
if (configured.includes(origin)) return true;
|
if (configured.includes(origin)) return true;
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user