mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 22:28:27 +00:00
refactor(security): unify local-host and tailnet CIDR checks
This commit is contained in:
@@ -12,9 +12,9 @@ import {
|
||||
type RateLimitCheckResult,
|
||||
} from "./auth-rate-limit.js";
|
||||
import {
|
||||
isLocalishHost,
|
||||
isLoopbackAddress,
|
||||
isTrustedProxyAddress,
|
||||
resolveHostName,
|
||||
resolveClientIp,
|
||||
} from "./net.js";
|
||||
|
||||
@@ -133,10 +133,6 @@ export function isLocalDirectRequest(
|
||||
return false;
|
||||
}
|
||||
|
||||
const host = resolveHostName(req.headers?.host);
|
||||
const hostIsLocal = host === "localhost" || host === "127.0.0.1" || host === "::1";
|
||||
const hostIsTailscaleServe = host.endsWith(".ts.net");
|
||||
|
||||
const hasForwarded = Boolean(
|
||||
req.headers?.["x-forwarded-for"] ||
|
||||
req.headers?.["x-real-ip"] ||
|
||||
@@ -144,7 +140,7 @@ export function isLocalDirectRequest(
|
||||
);
|
||||
|
||||
const remoteIsTrustedProxy = isTrustedProxyAddress(req.socket?.remoteAddress, trustedProxies);
|
||||
return (hostIsLocal || hostIsTailscaleServe) && (!hasForwarded || remoteIsTrustedProxy);
|
||||
return isLocalishHost(req.headers?.host) && (!hasForwarded || remoteIsTrustedProxy);
|
||||
}
|
||||
|
||||
function getTailscaleUser(req?: IncomingMessage): TailscaleUser | null {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os from "node:os";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
isLocalishHost,
|
||||
isPrivateOrLoopbackAddress,
|
||||
isSecureWebSocketUrl,
|
||||
isTrustedProxyAddress,
|
||||
@@ -24,6 +25,28 @@ describe("resolveHostName", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isLocalishHost", () => {
|
||||
it("accepts loopback and tailscale serve/funnel host headers", () => {
|
||||
const accepted = [
|
||||
"localhost",
|
||||
"127.0.0.1:18789",
|
||||
"[::1]:18789",
|
||||
"[::ffff:127.0.0.1]:18789",
|
||||
"gateway.tailnet.ts.net",
|
||||
];
|
||||
for (const host of accepted) {
|
||||
expect(isLocalishHost(host), host).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects non-local hosts", () => {
|
||||
const rejected = ["example.com", "192.168.1.10", "203.0.113.5:18789"];
|
||||
for (const host of rejected) {
|
||||
expect(isLocalishHost(host), host).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("isTrustedProxyAddress", () => {
|
||||
describe("exact IP matching", () => {
|
||||
it("returns true when IP matches exactly", () => {
|
||||
|
||||
@@ -334,6 +334,19 @@ export function isLoopbackHost(host: string): boolean {
|
||||
return isLoopbackAddress(unbracket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Local-facing host check for inbound requests:
|
||||
* - loopback hosts (localhost/127.x/::1 and mapped forms)
|
||||
* - Tailscale Serve/Funnel hostnames (*.ts.net)
|
||||
*/
|
||||
export function isLocalishHost(hostHeader?: string): boolean {
|
||||
const host = resolveHostName(hostHeader);
|
||||
if (!host) {
|
||||
return false;
|
||||
}
|
||||
return isLoopbackHost(host) || host.endsWith(".ts.net");
|
||||
}
|
||||
|
||||
/**
|
||||
* Security check for WebSocket URLs (CWE-319: Cleartext Transmission of Sensitive Information).
|
||||
*
|
||||
|
||||
@@ -41,8 +41,12 @@ import {
|
||||
mintCanvasCapabilityToken,
|
||||
} from "../../canvas-capability.js";
|
||||
import { buildDeviceAuthPayload } from "../../device-auth.js";
|
||||
import { isLoopbackAddress, isTrustedProxyAddress, resolveClientIp } from "../../net.js";
|
||||
import { resolveHostName } from "../../net.js";
|
||||
import {
|
||||
isLocalishHost,
|
||||
isLoopbackAddress,
|
||||
isTrustedProxyAddress,
|
||||
resolveClientIp,
|
||||
} from "../../net.js";
|
||||
import { resolveNodeCommandAllowlist } from "../../node-command-policy.js";
|
||||
import { checkBrowserOrigin } from "../../origin-check.js";
|
||||
import { GATEWAY_CLIENT_IDS } from "../../protocol/client-info.js";
|
||||
@@ -164,10 +168,7 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
const hasProxyHeaders = Boolean(forwardedFor || realIp);
|
||||
const remoteIsTrustedProxy = isTrustedProxyAddress(remoteAddr, trustedProxies);
|
||||
const hasUntrustedProxyHeaders = hasProxyHeaders && !remoteIsTrustedProxy;
|
||||
const hostName = resolveHostName(requestHost);
|
||||
const hostIsLocal = hostName === "localhost" || hostName === "127.0.0.1" || hostName === "::1";
|
||||
const hostIsTailscaleServe = hostName.endsWith(".ts.net");
|
||||
const hostIsLocalish = hostIsLocal || hostIsTailscaleServe;
|
||||
const hostIsLocalish = isLocalishHost(requestHost);
|
||||
const isLocalClient = isLocalDirectRequest(upgradeReq, trustedProxies, allowRealIpFallback);
|
||||
const reportedClientIp =
|
||||
isLocalClient || hasUntrustedProxyHeaders
|
||||
|
||||
Reference in New Issue
Block a user