mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 13:11:22 +00:00
fix(security): harden imessage remote scp/ssh handling
This commit is contained in:
19
src/infra/scp-host.test.ts
Normal file
19
src/infra/scp-host.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isSafeScpRemoteHost, normalizeScpRemoteHost } from "./scp-host.js";
|
||||
|
||||
describe("scp remote host", () => {
|
||||
it("accepts host and user@host forms", () => {
|
||||
expect(normalizeScpRemoteHost("gateway-host")).toBe("gateway-host");
|
||||
expect(normalizeScpRemoteHost("bot@gateway-host")).toBe("bot@gateway-host");
|
||||
expect(normalizeScpRemoteHost("bot@192.168.64.3")).toBe("bot@192.168.64.3");
|
||||
expect(normalizeScpRemoteHost("bot@[fe80::1]")).toBe("bot@[fe80::1]");
|
||||
});
|
||||
|
||||
it("rejects unsafe host tokens", () => {
|
||||
expect(isSafeScpRemoteHost("-oProxyCommand=whoami")).toBe(false);
|
||||
expect(isSafeScpRemoteHost("bot@gateway-host -oStrictHostKeyChecking=no")).toBe(false);
|
||||
expect(isSafeScpRemoteHost("bot@host:22")).toBe(false);
|
||||
expect(isSafeScpRemoteHost("bot@/tmp/host")).toBe(false);
|
||||
expect(isSafeScpRemoteHost("bot@@host")).toBe(false);
|
||||
});
|
||||
});
|
||||
62
src/infra/scp-host.ts
Normal file
62
src/infra/scp-host.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
const SSH_TOKEN = /^[A-Za-z0-9._-]+$/;
|
||||
const BRACKETED_IPV6 = /^\[[0-9A-Fa-f:.%]+\]$/;
|
||||
const WHITESPACE = /\s/;
|
||||
|
||||
function hasControlOrWhitespace(value: string): boolean {
|
||||
for (const char of value) {
|
||||
const code = char.charCodeAt(0);
|
||||
if (code <= 0x1f || code === 0x7f || WHITESPACE.test(char)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function normalizeScpRemoteHost(value: string | null | undefined): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
if (hasControlOrWhitespace(trimmed)) {
|
||||
return undefined;
|
||||
}
|
||||
if (trimmed.startsWith("-") || trimmed.includes("/") || trimmed.includes("\\")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const firstAt = trimmed.indexOf("@");
|
||||
const lastAt = trimmed.lastIndexOf("@");
|
||||
|
||||
let user: string | undefined;
|
||||
let host = trimmed;
|
||||
|
||||
if (firstAt !== -1) {
|
||||
if (firstAt !== lastAt || firstAt === 0 || firstAt === trimmed.length - 1) {
|
||||
return undefined;
|
||||
}
|
||||
user = trimmed.slice(0, firstAt);
|
||||
host = trimmed.slice(firstAt + 1);
|
||||
if (!SSH_TOKEN.test(user)) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (!host || host.startsWith("-") || host.includes("@")) {
|
||||
return undefined;
|
||||
}
|
||||
if (host.includes(":") && !BRACKETED_IPV6.test(host)) {
|
||||
return undefined;
|
||||
}
|
||||
if (!SSH_TOKEN.test(host) && !BRACKETED_IPV6.test(host)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return user ? `${user}@${host}` : host;
|
||||
}
|
||||
|
||||
export function isSafeScpRemoteHost(value: string | null | undefined): boolean {
|
||||
return normalizeScpRemoteHost(value) !== undefined;
|
||||
}
|
||||
@@ -135,7 +135,7 @@ export async function startSshPortForward(opts: {
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=accept-new",
|
||||
"StrictHostKeyChecking=yes",
|
||||
"-o",
|
||||
"UpdateHostKeys=yes",
|
||||
"-o",
|
||||
|
||||
Reference in New Issue
Block a user