Skip to content

fix(editor): accept plugin node kinds in the scene save API#490

Open
konevenkatesh wants to merge 1 commit into
pascalorg:mainfrom
konevenkatesh:fix/scene-api-plugin-node-kinds
Open

fix(editor): accept plugin node kinds in the scene save API#490
konevenkatesh wants to merge 1 commit into
pascalorg:mainfrom
konevenkatesh:fix/scene-api-plugin-node-kinds

Conversation

@konevenkatesh

@konevenkatesh konevenkatesh commented Jul 11, 2026

Copy link
Copy Markdown

Problem

Placing any plugin node (e.g. trees:tree from the first-party Nature plugin) in a saved scene makes every subsequent autosave fail with 400.

Two validation gaps in apiGraphSchema (apps/editor/lib/graph-schema.ts):

  1. Every node is validated against the static AnyNode union, which doesn't include namespaced plugin kinds — the plugin node itself is rejected (Invalid enum value. Expected 'wall' | …, received 'trees:tree').
  2. LevelNode.children only accepts builtin typed-id patterns, so a level containing a tree_… child id fails too.

Repro: run apps/editor, create a saved scene, place a tree from the Nature panel, watch PUT /api/scenes/[id] return 400 (the editor shows a persistent save error and no further changes persist).

Fix

  • Nodes whose type matches a namespaced plugin kind (ns:kind) validate against the BaseNode envelope plus a deep scan that rejects dangerous URL schemes (javascript:, vbscript:, file:, ftp:, non-image data:) anywhere in the node. This preserves the Phase 3 SSRF / script-URL posture without importing plugin/renderer code into the API route (plugin schemas live in packages that pull in UI code).
  • Builtin containers are validated against AnyNode with plugin child ids filtered from a copy — the stored graph is unchanged (a regression test pins this).

Verification

  • 8 new regression tests in apps/editor/lib/graph-schema.test.ts (5 of 8 fail without the fix): plugin node accepted, level-with-plugin-child accepted, parsed graph keeps plugin child ids, bad envelope rejected, dangerous URL schemes rejected (with data:image/… still allowed), invalid builtin nodes and non-namespaced unknown types still rejected.
  • End-to-end on the running app: placed a Nature oak in a saved scene — PUT /api/scenes/[id] returns 200 and the stored graph contains the trees:tree node. Replaying the identical payload against the pre-fix schema returns 400.
  • bun test apps/editor/lib/ 16/16 pass; biome check clean; no new type errors.

🤖 Generated with Claude Code


Note

Medium Risk
Changes untrusted graph validation on POST/PUT scene APIs; the new plugin path relies on envelope + heuristic URL scanning instead of full AnyNode/AssetUrl rules, so correctness of that scan matters for SSRF/script-URL posture.

Overview
Fixes 400 autosave failures when saved scenes contain plugin nodes (e.g. trees:tree) by extending apiGraphSchema so namespaced plugin kinds are no longer rejected by the static AnyNode union.

Plugin nodes now validate against a BaseNode envelope plus a recursive scan that blocks dangerous URL schemes (javascript:, file:, non-image data:, etc.) without importing full plugin schemas into API routes. Builtin nodes still use AnyNode, but plugin child ids are removed only on a validation copy so levels with plugin children pass while the stored graph keeps those child ids.

Adds regression tests for acceptance, child-id preservation, URL allow/deny rules, and unchanged rejection of invalid builtins and non-namespaced unknown types.

Reviewed by Cursor Bugbot for commit dbbfb0d. Bugbot is set up for automated code reviews on this repo. Configure here.

Nodes from registered plugins (namespaced type, e.g. trees:tree) failed
apiGraphSchema because the static AnyNode union doesn't know them, and
LevelNode.children only accepts builtin typed-id patterns — so placing
any Nature item made every scene autosave fail with 400.

Plugin kinds now validate against the BaseNode envelope plus a deep
scan rejecting dangerous URL schemes (javascript:, file:, data:text/…),
preserving the SSRF/script-URL posture without importing renderer code
into the API route. Builtin containers are validated with plugin child
ids filtered from a copy; the stored graph is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit dbbfb0d. Configure here.

path: (string | number)[],
): (string | number)[] | null {
if (typeof value === 'string') {
return DANGEROUS_STRING.test(value) ? path : null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plugin URL scan misses SSRF

Medium Severity

DANGEROUS_STRING only denylists javascript/vbscript/file/ftp and non-image data:, so plugin nodes can persist non-loopback http:// (and ws:) URLs that AssetUrl rejects on builtin fields. Combined with .passthrough(), crafted fields like config.textures[].src reopen the Phase 3 client-side beacon/SSRF class the comments claim this path preserves.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dbbfb0d. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant