mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-13 11:00:50 +00:00
88 lines
2.2 KiB
TypeScript
88 lines
2.2 KiB
TypeScript
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(): (
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction,
|
|
) => void {
|
|
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();
|
|
};
|
|
}
|