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,122 @@
|
||||
# Phase 8 P10 — full sweep (stdio MCP)
|
||||
|
||||
Generated: 2026-04-19T18:21:20.973Z
|
||||
|
||||
## Summary
|
||||
|
||||
- Transport: stdio (`bun packages/mcp/dist/bin/pascal-mcp.js --stdio`)
|
||||
- Data dir: `/tmp/pascal-phase8-p10`
|
||||
- Sampling: mocked via `client.setRequestHandler(CreateMessageRequestSchema, …)`
|
||||
- Tools listed: **30**
|
||||
- Resources listed: **3** static + **1** template
|
||||
- Prompts listed: **3**
|
||||
- Total entries exercised: **37**
|
||||
- PASS: **37** / PARTIAL: **0** / FAIL: **0**
|
||||
- Run time: **152 ms**
|
||||
|
||||
## Listed tools
|
||||
|
||||
```
|
||||
analyze_floorplan_image
|
||||
analyze_room_photo
|
||||
apply_patch
|
||||
check_collisions
|
||||
create_from_template
|
||||
create_level
|
||||
create_wall
|
||||
cut_opening
|
||||
delete_node
|
||||
delete_scene
|
||||
describe_node
|
||||
duplicate_level
|
||||
export_glb
|
||||
export_json
|
||||
find_nodes
|
||||
generate_variants
|
||||
get_node
|
||||
get_scene
|
||||
list_scenes
|
||||
list_templates
|
||||
load_scene
|
||||
measure
|
||||
photo_to_scene
|
||||
place_item
|
||||
redo
|
||||
rename_scene
|
||||
save_scene
|
||||
set_zone
|
||||
undo
|
||||
validate_scene
|
||||
```
|
||||
|
||||
## Listed resources
|
||||
|
||||
```
|
||||
(static)
|
||||
pascal://catalog/items
|
||||
pascal://scene/current
|
||||
pascal://scene/current/summary
|
||||
|
||||
(templates)
|
||||
pascal://constraints/{levelId}
|
||||
```
|
||||
|
||||
## Listed prompts
|
||||
|
||||
```
|
||||
from_brief
|
||||
iterate_on_feedback
|
||||
renovation_from_photos
|
||||
```
|
||||
|
||||
## Pass matrix — tools
|
||||
|
||||
| # | Tool | Status | Note |
|
||||
|---|------|--------|------|
|
||||
| 1 | `list_templates` | PASS | 3 templates |
|
||||
| 2 | `create_from_template` | PASS | templateId=two-bedroom nodes=25 |
|
||||
| 3 | `get_scene` | PASS | 25 nodes / 1 roots |
|
||||
| 4 | `get_node` | PASS | type=site |
|
||||
| 5 | `describe_node` | PASS | type=site children=1 |
|
||||
| 6 | `find_nodes` | PASS | 1 level(s) |
|
||||
| 7 | `measure` | PASS | d=6.403m |
|
||||
| 8 | `apply_patch` | PASS | applied=1 |
|
||||
| 9 | `create_level` | PASS | levelId=level_7fjtu570j6yyxcm4 |
|
||||
| 10 | `create_wall` | PASS | wallId=wall_2b2og8j9nrr6jk6t |
|
||||
| 11 | `place_item` | PASS | itemId=item_efdmfnownqwa3yd4 |
|
||||
| 12 | `cut_opening` | PASS | openingId=door_c0cul6bctrwf76md |
|
||||
| 13 | `set_zone` | PASS | zoneId=zone_7ljy8xitu0km2q8d |
|
||||
| 14 | `duplicate_level` | PASS | newLevelId=level_i5yq9hk2kwd9unpw nodes=28 |
|
||||
| 15 | `delete_node` | PASS | deleted 28 nodes |
|
||||
| 16 | `undo` | PASS | undone=1 |
|
||||
| 17 | `redo` | PASS | redone=1 |
|
||||
| 18 | `export_json` | PASS | 21051 chars |
|
||||
| 19 | `export_glb` | PASS | bytes/b64=0 |
|
||||
| 20 | `validate_scene` | PASS | valid=true errors=0 |
|
||||
| 21 | `check_collisions` | PASS | 0 collision(s) |
|
||||
| 22 | `analyze_floorplan_image` | PASS | walls=4 rooms=1 conf=0.82 |
|
||||
| 23 | `analyze_room_photo` | PASS | w=4m fixtures=2 |
|
||||
| 24 | `save_scene` | PASS | id=7b7ca35340e3 v=1 nodes=31 |
|
||||
| 25 | `list_scenes` | PASS | 1 scene(s) |
|
||||
| 26 | `load_scene` | PASS | id=7b7ca35340e3 nodes=31 |
|
||||
| 27 | `generate_variants` | PASS | 2 variants, ids=2 |
|
||||
| 28 | `photo_to_scene` | PASS | sceneId=fbde55412851 walls=4 rooms=1 |
|
||||
| 29 | `rename_scene` | PASS | name=p10 base renamed |
|
||||
| 30 | `delete_scene` | PASS | deleted=86366d07b242 |
|
||||
|
||||
## Pass matrix — resources
|
||||
|
||||
| # | Resource | Status | Note |
|
||||
|---|----------|--------|------|
|
||||
| 1 | `pascal://scene/current` | PASS | 8 nodes / 1 roots |
|
||||
| 2 | `pascal://scene/current/summary` | PASS | 425 chars of markdown |
|
||||
| 3 | `pascal://catalog/items` | PASS | 0 items |
|
||||
| 4 | `pascal://constraints/{levelId}` | PASS | slabs=0 wallPolys=4 |
|
||||
|
||||
## Pass matrix — prompts
|
||||
|
||||
| # | Prompt | Status | Note |
|
||||
|---|--------|--------|------|
|
||||
| 1 | `from_brief` | PASS | 1 message(s) |
|
||||
| 2 | `iterate_on_feedback` | PASS | 1 message(s) |
|
||||
| 3 | `renovation_from_photos` | PASS | 6 message(s) |
|
||||
Reference in New Issue
Block a user