Files
editor/packages/mcp/test-reports/phase8/p9-edges.md
T
Adrian PerezandClaude Opus 4.7 0b84e7b7b1 fix(mcp,editor): close URL-validation bypasses surfaced by Phase 8 P4
Phase 8 parallel validation flagged two boundaries where malicious URLs
(javascript:, file:, external http:, data:text/html, ...) could be
persisted despite the AssetUrl allowlist added in Phase 7 A7:

1. `save_scene({ includeCurrentScene: false, graph })` — the graph arg
   was treated as opaque (`z.record(z.string(), z.unknown())`) and
   written to the store without re-running AnyNode.safeParse.

2. `POST /api/scenes { graph }` in the editor API — same issue; the
   Zod `graphSchema` accepted anything object-shaped.

Fixes:
- `save-scene.ts`: when `includeCurrentScene === false`, iterate every
  node and run `AnyNode.safeParse`; collect issues and throw
  `McpError(InvalidParams, 'graph_invalid', { errors })` on any
  failure.
- `app/api/scenes/route.ts`: replace `graphSchema` with a structured
  `z.object({ nodes, rootNodeIds, collections? })` + `superRefine`
  that runs `AnyNode.safeParse` on every node. Invalid → 400 with
  detailed issue paths.

Tests:
- Added `save_scene` regression test for the P4 attack
  (item.asset.src = 'javascript:alert(1)') — expected error.
- Fixed the existing `includeCurrentScene=false` test to use a
  schema-compliant site node id (the prior `id: 'root'` now fails
  the AnyNode parse, which is the desired strict behaviour).
- Full suite: 294 pass / 0 fail.

Also adds Phase 8 test-reports/phase8/** (10 agents, ~15 scripts +
markdown reports) documenting the validation run, plus minor biome
cleanups to the Phase 5/7 test artefacts (removed stale
`// biome-ignore` suppression comments that now resolve to the
already-off `noConsole` rule).

Phase 8 result summary (10 parallel agents, stdio MCP transport with
isolated data dirs):
- P1 templates: 18/18 PASS
- P2 variants: 6/7 mutations + determinism + save + combined + error
- P3 locking: 12/12 PASS (MCP + editor HTTP If-Match)
- P4 URL hardening: fixed 2 bypasses (see above)
- P5 photo-to-scene: 6/6 PASS
- P6 Casa del Sol via save_scene: 13/13 PASS
- P7 editor HTTP API: 18/18 PASS
- P8 concurrency: 4/5 PASS, flagged 2 real filesystem-store races
  (expectedVersion CAS gap + .index.json drift under parallel writes)
- P9 edge cases: 13/13 PASS (size cap, slug safety, bad inputs)
- P10 full sweep: 37/37 PASS (30 tools + 4 resources + 3 prompts)

Known follow-ups:
- FilesystemSceneStore needs a proper lockfile / atomic CAS to fix
  the P8 concurrency bugs (low priority: single-writer MCP is the
  typical case).

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

3.1 KiB

Phase 8 P9 — edge cases & error-handling depth (stdio MCP)

Generated: 2026-04-19T18:20:30.398Z Transport: stdio (bun packages/mcp/dist/bin/pascal-mcp.js --stdio), data dir /tmp/pascal-phase8-p9.

Summary: 13/13 PASS, 0 WARN, 0 FAIL, 419 ms. Scene files on disk after bulk tests: 50

Test cases

# Case Status Detail
1 save 5k-node scene PASS nodeCount=5003 (expected 5003), sizeBytes=2392313, version=1
2 save 10 MB-ish scene rejected PASS tool_error text="MCP error -32600: Scene "too-big-scene" is 12754914 bytes, exceeds cap of 10485760 bytes"
3 save_scene path-traversal id PASS sanitised id="etcpasswd", fileExists=true, noEscape=true
4 save_scene dirty id sanitisation PASS sanitised id="upper-case", fileExists=true
5 save_scene empty id rejected PASS tool_error: MCP error -32602: Input validation error: Invalid arguments for tool save_scene: [ { "origin": "string", "code": "too_small", "minimum": 1, "inclusive": true, "path": [ "id…
6 save_scene empty name rejected PASS tool_error: MCP error -32602: Input validation error: Invalid arguments for tool save_scene: [ { "origin": "string", "code": "too_small", "minimum": 1, "inclusive": true, "path": [ "na…
7 save_scene name length 500 rejected PASS tool_error: MCP error -32602: Input validation error: Invalid arguments for tool save_scene: [ { "origin": "string", "code": "too_big", "maximum": 200, "inclusive": true, "path": [ "na…
8 create_from_template null id rejected PASS tool_error: MCP error -32602: Input validation error: Invalid arguments for tool create_from_template: [ { "expected": "string", "code": "invalid_type", "path": [ "id" ], "message": "I…
9 rename_scene empty newName rejected PASS tool_error: MCP error -32602: Input validation error: Invalid arguments for tool rename_scene: [ { "origin": "string", "code": "too_small", "minimum": 1, "inclusive": true, "path": [ "…
10 list 50 scenes updatedAt DESC PASS count=50, descOk=true
11 list_scenes limit=10 PASS count=10
12 list_scenes limit=-1 PASS rejected: MCP error -32602: Input validation error: Invalid arguments for tool list_scenes: [ { "origin": "number", "code": "too_small", "minimum": 0, "inclusive": false, "path": [ "…
13 PASCAL_DATA_DIR nonexistent root PASS auto-created=true, file=true, id=first-scene

Notes

  • Case 1 (5k nodes) constructs walls programmatically and saves via save_scene({ includeCurrentScene: false, graph }).
  • Case 2 pads metadata.padding on each of 500 walls to push past 10 MB. PASS = structured error mentioning too_large; WARN = other rejection reason.
  • Cases 3-5 exercise slug hygiene (sanitizeSlug in storage/slug.ts).
  • Case 13 spawns a second stdio child with a deep nonexistent data dir. PASS if the dir is auto-created by the filesystem store or the call fails with a clear error (ENOENT/EACCES/etc.).