Allow authenticated RPC requests from any origin

This commit is contained in:
musistudio
2026-07-28 20:09:19 +08:00
parent c0e4dba2c3
commit 6dff43dd15
2 changed files with 31 additions and 31 deletions
@@ -205,10 +205,6 @@ async function handleRpcRequest(request: IncomingMessage, response: ServerRespon
sendJson(response, 415, { error: { message: "RPC requests must use application/json." }, ok: false });
return;
}
if (!isAllowedWebRequestOrigin(request, security)) {
sendJson(response, 403, { error: { message: "Forbidden RPC origin." }, ok: false });
return;
}
if (!hasValidWebAuthToken(request, security)) {
sendJson(response, 401, { error: { message: "CCR web authentication token is missing or invalid." }, ok: false });
return;
@@ -602,32 +598,6 @@ function isAllowedWebRequestHost(request: IncomingMessage, security: WebManageme
return Boolean(hostname && isAllowedWebHostname(hostname, security));
}
function isAllowedWebRequestOrigin(request: IncomingMessage, security: WebManagementSecurityContext): boolean {
const origin = readHeaderValue(request.headers.origin);
if (origin && !isAllowedWebOriginValue(origin, security)) {
return false;
}
const referer = readHeaderValue(request.headers.referer);
if (!origin && referer && !isAllowedWebOriginValue(referer, security)) {
return false;
}
return true;
}
function isAllowedWebOriginValue(value: string, security: WebManagementSecurityContext): boolean {
try {
const url = new URL(value);
const port = url.port ? Number(url.port) : url.protocol === "http:" ? 80 : url.protocol === "https:" ? 443 : undefined;
return url.protocol === "http:" &&
port === security.port &&
isAllowedWebHostname(normalizeHostname(url.hostname), security);
} catch {
return false;
}
}
function isAllowedWebHostname(hostname: string, security: WebManagementSecurityContext): boolean {
const normalized = normalizeHostname(hostname);
return security.allowedHostnames.has(normalized) ||
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import test from "node:test";
import { normalizeExternalHttpTarget } from "@ccr/core/web/management-server.ts";
import { normalizeExternalHttpTarget, startWebManagementServer } from "@ccr/core/web/management-server.ts";
test("normalizeExternalHttpTarget accepts absolute http, https, and CCR plugin URLs only", () => {
assert.equal(normalizeExternalHttpTarget(""), undefined);
@@ -12,3 +12,33 @@ test("normalizeExternalHttpTarget accepts absolute http, https, and CCR plugin U
assert.throws(() => normalizeExternalHttpTarget("javascript:alert(1)"), /Only http, https, and CCR plugin URLs/);
assert.throws(() => normalizeExternalHttpTarget("example.com"), /valid absolute URL/);
});
test("web RPC ignores Origin and Referer when the auth token is valid", async () => {
const authToken = "test-web-auth-token";
const runtime = await startWebManagementServer({
authToken,
host: "127.0.0.1",
port: 0,
startGateway: false
});
try {
const endpoint = new URL("/api/ccr/rpc", runtime.url);
const response = await fetch(endpoint, {
body: JSON.stringify({ args: [], method: "getAppInfo" }),
headers: {
"content-type": "application/json",
"origin": "http://127.0.0.1:8000",
"referer": "http://127.0.0.1:8000/",
"x-ccr-web-auth": authToken
},
method: "POST"
});
const payload = await response.json();
assert.equal(response.status, 200);
assert.equal(payload.ok, true);
assert.equal(payload.value.name, "Claude Code Router");
} finally {
await runtime.close();
}
});