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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
e8d0b13ff5
commit
0b84e7b7b1
@@ -0,0 +1,77 @@
|
||||
# Phase 8 P6 — Casa del Sol via save_scene
|
||||
|
||||
Generated: 2026-04-19T18:20:19.643Z
|
||||
Transport: stdio (spawned `bun /Users/adrian/Desktop/editor/.worktrees/mcp-server/packages/mcp/dist/bin/pascal-mcp.js --stdio`)
|
||||
PASCAL_DATA_DIR: `/tmp/pascal-phase8`
|
||||
Editor URL: http://localhost:3002
|
||||
|
||||
## Result summary
|
||||
|
||||
- Steps passed: **13/13**
|
||||
- Initial node count: 3
|
||||
- Final node count: **39** (threshold ≥ 30)
|
||||
- doors=6, windows=6, zones=9, walls=9, fences=5, slabs=1
|
||||
- All validate_scene calls valid=true: **true**
|
||||
- Saved scene id: `6f87c59c1535`
|
||||
- Scene file: `/tmp/pascal-phase8/scenes/6f87c59c1535.json`
|
||||
|
||||
### Open in browser: http://localhost:3002/scene/6f87c59c1535
|
||||
|
||||
## Per-step results
|
||||
|
||||
| # | Step | Status | Duration | Summary |
|
||||
|---|------|--------|----------|---------|
|
||||
| 1 | discover | PASS | 2ms | building=building_16mw8oy88f952is9, level=level_3jbpcuma0wfwclex |
|
||||
| 2 | perimeter walls | PASS | 2ms | 4 walls |
|
||||
| 3 | interior walls | PASS | 0ms | 5 walls |
|
||||
| 4 | zones | PASS | 2ms | 7 zones |
|
||||
| 5 | openings | PASS | 3ms | 6 doors, 6 windows, 0 failures |
|
||||
| 6 | pool zone + slab | PASS | 1ms | zone=zone_lnki9rxnoyq72rwi, slab=slab_rfyzzvcxlkbgif1w |
|
||||
| 7 | privacy fences | PASS | 1ms | 5 fences |
|
||||
| 8 | garden zone | PASS | 0ms | zone=zone_6l4ls8mr4rtfvsye |
|
||||
| 9 | save_scene | PASS | 6ms | id=6f87c59c1535, nodeCount=39, size=29677B, url=/scene/6f87c59c1535 |
|
||||
| 10 | file on disk | PASS | 0ms | /tmp/pascal-phase8/scenes/6f87c59c1535.json (29677B) |
|
||||
| 11 | GET /api/scenes/:id | PASS | 9ms | 200 OK, 39 nodes |
|
||||
| 12 | GET /scene/:id (HTML) | PASS | 237ms | 200 OK, text/html; charset=utf-8, 72918B |
|
||||
| 13 | write v2 scene.json | PASS | 1ms | 26761B -> /Users/adrian/Desktop/editor/.worktrees/mcp-server/packages/mcp/test-reports/phase8/casa-sol-v2.json |
|
||||
|
||||
## Validation history
|
||||
|
||||
| Phase | valid | errors |
|
||||
|-------|-------|--------|
|
||||
| initial | true | 0 |
|
||||
| afterWalls | true | 0 |
|
||||
| afterZones | true | 0 |
|
||||
| afterOpenings | true | 0 |
|
||||
| afterPool | true | 0 |
|
||||
| afterFences | true | 0 |
|
||||
| afterGarden | true | 0 |
|
||||
| final | true | 0 |
|
||||
|
||||
## save_scene response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "6f87c59c1535",
|
||||
"name": "Casa del Sol",
|
||||
"projectId": null,
|
||||
"thumbnailUrl": null,
|
||||
"version": 1,
|
||||
"createdAt": "2026-04-19T18:20:19.389Z",
|
||||
"updatedAt": "2026-04-19T18:20:19.389Z",
|
||||
"ownerId": null,
|
||||
"sizeBytes": 29677,
|
||||
"nodeCount": 39,
|
||||
"url": "/scene/6f87c59c1535"
|
||||
}
|
||||
```
|
||||
|
||||
## Assertions
|
||||
|
||||
- [x] ≥30 nodes total
|
||||
- [x] validate_scene valid:true at every phase
|
||||
- [x] save_scene returned id=`6f87c59c1535`
|
||||
- [x] file exists on disk at `/tmp/pascal-phase8/scenes/6f87c59c1535.json`
|
||||
- [x] GET /api/scenes/<id> returned 200 with matching node count
|
||||
- [x] GET /scene/<id> returned 200 HTML
|
||||
- [x] wrote `/Users/adrian/Desktop/editor/.worktrees/mcp-server/packages/mcp/test-reports/phase8/casa-sol-v2.json`
|
||||
Reference in New Issue
Block a user