mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 18:08:27 +00:00
fix(security): block cross-origin mutations on loopback browser routes
This commit is contained in:
83
src/browser/csrf.ts
Normal file
83
src/browser/csrf.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import { isLoopbackHost } from "../gateway/net.js";
|
||||
|
||||
function firstHeader(value: string | string[] | undefined): string {
|
||||
return Array.isArray(value) ? (value[0] ?? "") : (value ?? "");
|
||||
}
|
||||
|
||||
function isMutatingMethod(method: string): boolean {
|
||||
const m = (method || "").trim().toUpperCase();
|
||||
return m === "POST" || m === "PUT" || m === "PATCH" || m === "DELETE";
|
||||
}
|
||||
|
||||
function isLoopbackUrl(value: string): boolean {
|
||||
const v = value.trim();
|
||||
if (!v || v === "null") {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(v);
|
||||
return isLoopbackHost(parsed.hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldRejectBrowserMutation(params: {
|
||||
method: string;
|
||||
origin?: string;
|
||||
referer?: string;
|
||||
secFetchSite?: string;
|
||||
}): boolean {
|
||||
if (!isMutatingMethod(params.method)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Strong signal when present: browser says this is cross-site.
|
||||
// Avoid being overly clever with "same-site" since localhost vs 127.0.0.1 may differ.
|
||||
const secFetchSite = (params.secFetchSite ?? "").trim().toLowerCase();
|
||||
if (secFetchSite === "cross-site") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const origin = (params.origin ?? "").trim();
|
||||
if (origin) {
|
||||
return !isLoopbackUrl(origin);
|
||||
}
|
||||
|
||||
const referer = (params.referer ?? "").trim();
|
||||
if (referer) {
|
||||
return !isLoopbackUrl(referer);
|
||||
}
|
||||
|
||||
// Non-browser clients (curl/undici/Node) typically send no Origin/Referer.
|
||||
return false;
|
||||
}
|
||||
|
||||
export function browserMutationGuardMiddleware() {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
// OPTIONS is used for CORS preflight. Even if cross-origin, the preflight isn't mutating.
|
||||
const method = (req.method || "").trim().toUpperCase();
|
||||
if (method === "OPTIONS") {
|
||||
return next();
|
||||
}
|
||||
|
||||
const origin = firstHeader(req.headers.origin);
|
||||
const referer = firstHeader(req.headers.referer);
|
||||
const secFetchSite = firstHeader(req.headers["sec-fetch-site"]);
|
||||
|
||||
if (
|
||||
shouldRejectBrowserMutation({
|
||||
method,
|
||||
origin,
|
||||
referer,
|
||||
secFetchSite,
|
||||
})
|
||||
) {
|
||||
res.status(403).send("Forbidden");
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user