Files
editor/apps/editor/lib/graph-schema.ts
T
Adrian PerezandClaude Opus 4.7 8757de0c36 fix(mcp,editor): close PUT-route URL bypass + vision-tool SSRF (Phase 10 A2)
Pre-push audit agent A2 flagged two HIGH-severity security issues that
would have shipped in the PR had we not checked:

1. PUT /api/scenes/[id] URL-validation bypass. Phase 8 P4 fixed
   the POST /api/scenes route by replacing a loose z.unknown() graph
   schema with AnyNode superRefine. The fix never made it to the PUT
   handler - an attacker could resubmit the same javascript:/file:///
   payloads via PUT. Fixed by extracting the tight validator into
   apps/editor/lib/graph-schema.ts and sharing it across both routes.

2. SSRF in photo_to_scene / analyze_floorplan_image /
   analyze_room_photo. All three tools called raw fetch(image) on
   user-supplied URLs with no validation - a direct
   http://169.254.169.254/latest/meta-data/ exfil primitive on any
   cloud host. Added packages/mcp/src/lib/safe-fetch.ts that:
   - Blocks loopback (127.0.0.0/8, ::1)
   - Blocks link-local incl. cloud metadata (169.254.0.0/16)
   - Blocks private ranges (10/8, 172.16/12, 192.168/16, fc00::/7)
   - Blocks .local/.internal/.corp hostnames + localhost variants
   - Blocks v4-mapped IPv6 loopback (::ffff:127.0.0.1)
   - Manual redirects (max 3), revalidating the allowlist per hop
   - 20 MB response-size cap (streamed, enforced per-chunk)
   - 10s timeout
   - Optional PASCAL_ALLOWED_ASSET_ORIGINS env allowlist

Tests: 8 new SSRF guard tests, all vision tests still pass, full
suite 302/302.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:59:26 +02:00

35 lines
1.1 KiB
TypeScript

import { AnyNode } from '@pascal-app/core/schema'
import { z } from 'zod'
/**
* Validates a SceneGraph at an untrusted API boundary. Re-runs
* `AnyNode.safeParse` on every node, which enforces the `AssetUrl`
* allowlist in core (closes the Phase 3 SSRF / arbitrary-URL risk on
* scan/guide/item/material fields).
*
* Shared between `POST /api/scenes` and `PUT /api/scenes/[id]` so neither
* route can silently accept malicious URLs via the `graph` payload.
*
* Phase 8 P4 found the POST bypass; Phase 10 A2 found the PUT bypass.
*/
export const apiGraphSchema = z
.object({
nodes: z.record(z.string(), z.unknown()),
rootNodeIds: z.array(z.string()),
collections: z.unknown().optional(),
})
.superRefine((value, ctx) => {
for (const [nodeId, node] of Object.entries(value.nodes)) {
const res = AnyNode.safeParse(node)
if (!res.success) {
for (const issue of res.error.issues) {
ctx.addIssue({
code: 'custom',
path: ['nodes', nodeId, ...issue.path],
message: issue.message,
})
}
}
}
})