mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:31:06 +00:00
fix(device-pair): validate public setup urls (#74538)
* fix(device-pair): validate public setup urls * test(cli): cover invalid qr override urls --------- Co-authored-by: Lucenx9 <185146821+Lucenx9@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -609,6 +609,72 @@ describe("device-pair /pair default setup code", () => {
|
||||
text: "⚠️ This command requires operator.pairing for internal gateway callers.",
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes bare publicUrl host ports before issuing setup codes", async () => {
|
||||
const command = registerPairCommand({
|
||||
pluginConfig: {
|
||||
publicUrl: "gateway.example.test:18789/setup",
|
||||
},
|
||||
});
|
||||
const result = await command.handler(
|
||||
createCommandContext({
|
||||
channel: "webchat",
|
||||
args: "",
|
||||
commandBody: "/pair",
|
||||
gatewayClientScopes: ["operator.write", "operator.pairing"],
|
||||
}),
|
||||
);
|
||||
const text = requireText(result);
|
||||
|
||||
expect(pluginApiMocks.issueDeviceBootstrapToken).toHaveBeenCalledTimes(1);
|
||||
expect(text).toContain("Gateway: ws://gateway.example.test:18789");
|
||||
});
|
||||
|
||||
it("rejects invalid bare publicUrl host ports", async () => {
|
||||
const command = registerPairCommand({
|
||||
pluginConfig: {
|
||||
publicUrl: "localhost:notaport",
|
||||
},
|
||||
});
|
||||
const result = await command.handler(
|
||||
createCommandContext({
|
||||
channel: "webchat",
|
||||
args: "",
|
||||
commandBody: "/pair",
|
||||
gatewayClientScopes: ["operator.write", "operator.pairing"],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(pluginApiMocks.issueDeviceBootstrapToken).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ text: "Error: Configured publicUrl is invalid." });
|
||||
});
|
||||
|
||||
it.each([
|
||||
"http://localhost:notaport",
|
||||
"http:gateway.example.test",
|
||||
"ws:gateway.example.test",
|
||||
"http:/localhost:notaport",
|
||||
"ftp:/gateway.example.test",
|
||||
"mailto:foo@example.com",
|
||||
"ws://user:pass@gateway.example.test:18789",
|
||||
])("rejects invalid publicUrl %s before issuing setup codes", async (publicUrl) => {
|
||||
const command = registerPairCommand({
|
||||
pluginConfig: {
|
||||
publicUrl,
|
||||
},
|
||||
});
|
||||
const result = await command.handler(
|
||||
createCommandContext({
|
||||
channel: "webchat",
|
||||
args: "",
|
||||
commandBody: "/pair",
|
||||
gatewayClientScopes: ["operator.write", "operator.pairing"],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(pluginApiMocks.issueDeviceBootstrapToken).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ text: "Error: Configured publicUrl is invalid." });
|
||||
});
|
||||
});
|
||||
|
||||
describe("device-pair notify pending formatting", () => {
|
||||
|
||||
@@ -129,22 +129,34 @@ const QR_CHANNEL_SENDERS: Record<string, QrChannelSender> = {
|
||||
},
|
||||
};
|
||||
|
||||
const GATEWAY_SCHEME_WITHOUT_AUTHORITY_RE = /^(?:https?|wss?):(?!\/\/)/i;
|
||||
const SCHEME_LIKE_PATH_RE = /^[A-Za-z][A-Za-z0-9+.-]*:\//;
|
||||
|
||||
function normalizeUrl(raw: string, schemeFallback: "ws" | "wss"): string | null {
|
||||
const candidate = normalizeOptionalString(raw);
|
||||
if (!candidate) {
|
||||
return null;
|
||||
}
|
||||
if (GATEWAY_SCHEME_WITHOUT_AUTHORITY_RE.test(candidate)) {
|
||||
return null;
|
||||
}
|
||||
const parsedUrl = parseNormalizedGatewayUrl(candidate);
|
||||
if (parsedUrl) {
|
||||
return parsedUrl;
|
||||
}
|
||||
if (candidate.includes("://") || SCHEME_LIKE_PATH_RE.test(candidate)) {
|
||||
return null;
|
||||
}
|
||||
const hostPort = normalizeOptionalString(candidate.split("/", 1)[0]) ?? "";
|
||||
return hostPort ? `${schemeFallback}://${hostPort}` : null;
|
||||
return hostPort ? parseNormalizedGatewayUrl(`${schemeFallback}://${hostPort}`) : null;
|
||||
}
|
||||
|
||||
function parseNormalizedGatewayUrl(raw: string): string | null {
|
||||
try {
|
||||
const parsed = new URL(raw);
|
||||
if (parsed.username || parsed.password) {
|
||||
return null;
|
||||
}
|
||||
const scheme = parsed.protocol.slice(0, -1);
|
||||
const normalizedScheme = scheme === "http" ? "ws" : scheme === "https" ? "wss" : scheme;
|
||||
if (!(normalizedScheme === "ws" || normalizedScheme === "wss")) {
|
||||
|
||||
Reference in New Issue
Block a user