fix(security): fail-close node camera URL downloads

This commit is contained in:
Peter Steinberger
2026-03-02 23:23:30 +00:00
parent 7365aefa19
commit 3bf19d6f40
9 changed files with 302 additions and 74 deletions

View File

@@ -28,7 +28,7 @@ import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
import { sanitizeToolResultImages } from "../tool-images.js";
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
import { callGatewayTool, readGatewayCallOptions } from "./gateway.js";
import { listNodes, resolveNodeIdFromList, resolveNodeId } from "./nodes-utils.js";
import { listNodes, resolveNode, resolveNodeId, resolveNodeIdFromList } from "./nodes-utils.js";
const NODES_TOOL_ACTIONS = [
"status",
@@ -230,7 +230,8 @@ export function createNodesTool(options?: {
}
case "camera_snap": {
const node = readStringParam(params, "node", { required: true });
const nodeId = await resolveNodeId(gatewayOpts, node);
const resolvedNode = await resolveNode(gatewayOpts, node);
const nodeId = resolvedNode.nodeId;
const facingRaw =
typeof params.facing === "string" ? params.facing.toLowerCase() : "front";
const facings: CameraFacing[] =
@@ -295,7 +296,12 @@ export function createNodesTool(options?: {
ext: isJpeg ? "jpg" : "png",
});
if (payload.url) {
await writeUrlToFile(filePath, payload.url);
if (!resolvedNode.remoteIp) {
throw new Error("camera URL payload requires node remoteIp");
}
await writeUrlToFile(filePath, payload.url, {
expectedHost: resolvedNode.remoteIp,
});
} else if (payload.base64) {
await writeBase64ToFile(filePath, payload.base64);
}
@@ -373,7 +379,8 @@ export function createNodesTool(options?: {
}
case "camera_clip": {
const node = readStringParam(params, "node", { required: true });
const nodeId = await resolveNodeId(gatewayOpts, node);
const resolvedNode = await resolveNode(gatewayOpts, node);
const nodeId = resolvedNode.nodeId;
const facing =
typeof params.facing === "string" ? params.facing.toLowerCase() : "front";
if (facing !== "front" && facing !== "back") {
@@ -407,6 +414,7 @@ export function createNodesTool(options?: {
const filePath = await writeCameraClipPayloadToFile({
payload,
facing,
expectedHost: resolvedNode.remoteIp,
});
return {
content: [{ type: "text", text: `FILE:${filePath}` }],

View File

@@ -160,6 +160,15 @@ export async function resolveNodeId(
query?: string,
allowDefault = false,
) {
const nodes = await loadNodes(opts);
return resolveNodeIdFromList(nodes, query, allowDefault);
return (await resolveNode(opts, query, allowDefault)).nodeId;
}
export async function resolveNode(
opts: GatewayCallOptions,
query?: string,
allowDefault = false,
): Promise<NodeListNode> {
const nodes = await loadNodes(opts);
const nodeId = resolveNodeIdFromList(nodes, query, allowDefault);
return nodes.find((node) => node.nodeId === nodeId) ?? { nodeId };
}