mirror of
https://github.com/grp06/openclaw-studio.git
synced 2026-08-14 08:52:03 +00:00
Harden proxy browser auth passthrough detection
This commit is contained in:
+32
-4
@@ -49,12 +49,36 @@ const hasNonEmptyToken = (params) => {
|
||||
return typeof raw === "string" && raw.trim().length > 0;
|
||||
};
|
||||
|
||||
const hasDeviceSignature = (params) => {
|
||||
const raw =
|
||||
params && isObject(params) && isObject(params.device) ? params.device.signature : null;
|
||||
const hasNonEmptyPassword = (params) => {
|
||||
const raw = params && isObject(params) && isObject(params.auth) ? params.auth.password : "";
|
||||
return typeof raw === "string" && raw.trim().length > 0;
|
||||
};
|
||||
|
||||
const hasNonEmptyDeviceToken = (params) => {
|
||||
const raw = params && isObject(params) && isObject(params.auth) ? params.auth.deviceToken : "";
|
||||
return typeof raw === "string" && raw.trim().length > 0;
|
||||
};
|
||||
|
||||
const hasCompleteDeviceAuth = (params) => {
|
||||
const device = params && isObject(params) && isObject(params.device) ? params.device : null;
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
const id = typeof device.id === "string" ? device.id.trim() : "";
|
||||
const publicKey = typeof device.publicKey === "string" ? device.publicKey.trim() : "";
|
||||
const signature = typeof device.signature === "string" ? device.signature.trim() : "";
|
||||
const nonce = typeof device.nonce === "string" ? device.nonce.trim() : "";
|
||||
const signedAt = device.signedAt;
|
||||
return (
|
||||
id.length > 0 &&
|
||||
publicKey.length > 0 &&
|
||||
signature.length > 0 &&
|
||||
nonce.length > 0 &&
|
||||
Number.isFinite(signedAt) &&
|
||||
signedAt >= 0
|
||||
);
|
||||
};
|
||||
|
||||
function createGatewayProxy(options) {
|
||||
const {
|
||||
loadUpstreamSettings,
|
||||
@@ -118,7 +142,11 @@ function createGatewayProxy(options) {
|
||||
return;
|
||||
}
|
||||
connectRequestId = id;
|
||||
const browserHasAuth = hasNonEmptyToken(parsed.params) || hasDeviceSignature(parsed.params);
|
||||
const browserHasAuth =
|
||||
hasNonEmptyToken(parsed.params) ||
|
||||
hasNonEmptyPassword(parsed.params) ||
|
||||
hasNonEmptyDeviceToken(parsed.params) ||
|
||||
hasCompleteDeviceAuth(parsed.params);
|
||||
|
||||
let upstreamUrl = "";
|
||||
let upstreamToken = "";
|
||||
|
||||
@@ -255,12 +255,20 @@ describe("createGatewayProxy", () => {
|
||||
|
||||
let seenToken: string | null = null;
|
||||
let seenDeviceSignature: string | null = null;
|
||||
let seenDeviceId: string | null = null;
|
||||
let seenDevicePublicKey: string | null = null;
|
||||
let seenDeviceNonce: string | null = null;
|
||||
let seenDeviceSignedAt: number | null = null;
|
||||
upstream.on("connection", (ws) => {
|
||||
ws.on("message", (raw) => {
|
||||
const parsed = JSON.parse(String(raw));
|
||||
if (parsed?.method === "connect") {
|
||||
seenToken = parsed?.params?.auth?.token ?? null;
|
||||
seenDeviceSignature = parsed?.params?.device?.signature ?? null;
|
||||
seenDeviceId = parsed?.params?.device?.id ?? null;
|
||||
seenDevicePublicKey = parsed?.params?.device?.publicKey ?? null;
|
||||
seenDeviceNonce = parsed?.params?.device?.nonce ?? null;
|
||||
seenDeviceSignedAt = parsed?.params?.device?.signedAt ?? null;
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "res",
|
||||
@@ -297,7 +305,15 @@ describe("createGatewayProxy", () => {
|
||||
type: "req",
|
||||
id: "connect-pass-device",
|
||||
method: "connect",
|
||||
params: { device: { signature: "device-signature-123" } },
|
||||
params: {
|
||||
device: {
|
||||
id: "device-id-123",
|
||||
publicKey: "device-public-key-123",
|
||||
signature: "device-signature-123",
|
||||
signedAt: Date.now(),
|
||||
nonce: "device-nonce-123",
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -305,6 +321,156 @@ describe("createGatewayProxy", () => {
|
||||
const response = JSON.parse(String(rawMessage ?? ""));
|
||||
expect(response).toMatchObject({ type: "res", id: "connect-pass-device", ok: true });
|
||||
expect(seenDeviceSignature).toBe("device-signature-123");
|
||||
expect(seenDeviceId).toBe("device-id-123");
|
||||
expect(seenDevicePublicKey).toBe("device-public-key-123");
|
||||
expect(seenDeviceNonce).toBe("device-nonce-123");
|
||||
expect(typeof seenDeviceSignedAt).toBe("number");
|
||||
expect(seenToken).toBeNull();
|
||||
} finally {
|
||||
for (const client of upstream.clients) {
|
||||
client.close();
|
||||
}
|
||||
await Promise.all([
|
||||
closeWebSocket(browser),
|
||||
closeWebSocketServer(upstream),
|
||||
closeHttpServer(proxyHttp),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it("allows browser password passthrough when host token is missing", async () => {
|
||||
const upstream = new WebSocketServer({ port: 0 });
|
||||
const address = upstream.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("expected upstream server to have a port");
|
||||
}
|
||||
const upstreamUrl = `ws://127.0.0.1:${address.port}`;
|
||||
|
||||
let seenPassword: string | null = null;
|
||||
let seenToken: string | null = null;
|
||||
upstream.on("connection", (ws) => {
|
||||
ws.on("message", (raw) => {
|
||||
const parsed = JSON.parse(String(raw));
|
||||
if (parsed?.method === "connect") {
|
||||
seenPassword = parsed?.params?.auth?.password ?? null;
|
||||
seenToken = parsed?.params?.auth?.token ?? null;
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "res",
|
||||
id: parsed.id,
|
||||
ok: true,
|
||||
payload: { type: "hello-ok", protocol: 3, auth: {} },
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const { createGatewayProxy } = await import("../../server/gateway-proxy");
|
||||
|
||||
const proxyHttp = await import("node:http").then((m) => m.createServer());
|
||||
const proxy = createGatewayProxy({
|
||||
loadUpstreamSettings: async () => ({ url: upstreamUrl, token: "" }),
|
||||
allowWs: (req: { url?: string }) => req.url === "/api/gateway/ws",
|
||||
logError: () => {},
|
||||
});
|
||||
proxyHttp.on("upgrade", (req, socket, head) => proxy.handleUpgrade(req, socket, head));
|
||||
|
||||
await new Promise<void>((resolve) => proxyHttp.listen(0, "127.0.0.1", resolve));
|
||||
const proxyAddr = proxyHttp.address();
|
||||
if (!proxyAddr || typeof proxyAddr === "string") {
|
||||
throw new Error("expected proxy server to have a port");
|
||||
}
|
||||
|
||||
const browser = new WebSocket(`ws://127.0.0.1:${proxyAddr.port}/api/gateway/ws`);
|
||||
try {
|
||||
await waitForEvent(browser, "open");
|
||||
browser.send(
|
||||
JSON.stringify({
|
||||
type: "req",
|
||||
id: "connect-pass-password",
|
||||
method: "connect",
|
||||
params: { auth: { password: "browser-password-123" } },
|
||||
})
|
||||
);
|
||||
|
||||
const [rawMessage] = await waitForEvent<[WebSocket.RawData]>(browser, "message");
|
||||
const response = JSON.parse(String(rawMessage ?? ""));
|
||||
expect(response).toMatchObject({ type: "res", id: "connect-pass-password", ok: true });
|
||||
expect(seenPassword).toBe("browser-password-123");
|
||||
expect(seenToken).toBeNull();
|
||||
} finally {
|
||||
for (const client of upstream.clients) {
|
||||
client.close();
|
||||
}
|
||||
await Promise.all([
|
||||
closeWebSocket(browser),
|
||||
closeWebSocketServer(upstream),
|
||||
closeHttpServer(proxyHttp),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it("allows browser deviceToken passthrough when host token is missing", async () => {
|
||||
const upstream = new WebSocketServer({ port: 0 });
|
||||
const address = upstream.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("expected upstream server to have a port");
|
||||
}
|
||||
const upstreamUrl = `ws://127.0.0.1:${address.port}`;
|
||||
|
||||
let seenDeviceToken: string | null = null;
|
||||
let seenToken: string | null = null;
|
||||
upstream.on("connection", (ws) => {
|
||||
ws.on("message", (raw) => {
|
||||
const parsed = JSON.parse(String(raw));
|
||||
if (parsed?.method === "connect") {
|
||||
seenDeviceToken = parsed?.params?.auth?.deviceToken ?? null;
|
||||
seenToken = parsed?.params?.auth?.token ?? null;
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "res",
|
||||
id: parsed.id,
|
||||
ok: true,
|
||||
payload: { type: "hello-ok", protocol: 3, auth: {} },
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const { createGatewayProxy } = await import("../../server/gateway-proxy");
|
||||
|
||||
const proxyHttp = await import("node:http").then((m) => m.createServer());
|
||||
const proxy = createGatewayProxy({
|
||||
loadUpstreamSettings: async () => ({ url: upstreamUrl, token: "" }),
|
||||
allowWs: (req: { url?: string }) => req.url === "/api/gateway/ws",
|
||||
logError: () => {},
|
||||
});
|
||||
proxyHttp.on("upgrade", (req, socket, head) => proxy.handleUpgrade(req, socket, head));
|
||||
|
||||
await new Promise<void>((resolve) => proxyHttp.listen(0, "127.0.0.1", resolve));
|
||||
const proxyAddr = proxyHttp.address();
|
||||
if (!proxyAddr || typeof proxyAddr === "string") {
|
||||
throw new Error("expected proxy server to have a port");
|
||||
}
|
||||
|
||||
const browser = new WebSocket(`ws://127.0.0.1:${proxyAddr.port}/api/gateway/ws`);
|
||||
try {
|
||||
await waitForEvent(browser, "open");
|
||||
browser.send(
|
||||
JSON.stringify({
|
||||
type: "req",
|
||||
id: "connect-pass-device-token",
|
||||
method: "connect",
|
||||
params: { auth: { deviceToken: "browser-device-token-123" } },
|
||||
})
|
||||
);
|
||||
|
||||
const [rawMessage] = await waitForEvent<[WebSocket.RawData]>(browser, "message");
|
||||
const response = JSON.parse(String(rawMessage ?? ""));
|
||||
expect(response).toMatchObject({ type: "res", id: "connect-pass-device-token", ok: true });
|
||||
expect(seenDeviceToken).toBe("browser-device-token-123");
|
||||
expect(seenToken).toBeNull();
|
||||
} finally {
|
||||
for (const client of upstream.clients) {
|
||||
|
||||
Reference in New Issue
Block a user