From 6d73d583795f1c4ebddc5c4560c47241302f20f4 Mon Sep 17 00:00:00 2001 From: vanelsas <58037137+avanelsas@users.noreply.github.com> Date: Tue, 26 May 2026 16:26:06 +0200 Subject: [PATCH] review pass: load-boundary hardening + codegen dedup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route IDB autosave restore through `project-file/classify-payload` + `sanitize-doc` so a corrupted or tampered autosave can't bypass the checks the file-open path uses. Strict-reject unknown `:version` so a future schema bump fails loud instead of partially installing the wrong shape. Cancel the watcher-armed 750ms timer on file load so a stale `save-now!` can't fire after `clear-autosave!`. Surface autosave write failures as `[:ui :autosave-failed?]` with a hidden ⚠ indicator in the toolbar. Extend `unsafe-findings` with an identifier-shape scanner so attr keys, binding prop names, binding `:field`, trigger `:action-ref`, payload `:field`, field-def `:name`, and action `:name` that carry characters unsafe for codegen are refused at the load boundary — closes the JS / CLJS string-injection vectors in both export plugins. Extract `action-ref-canonical-ns` + `action-ref-alias` to `export.model` (single source of truth shared by both plugins). Route `cljs_project/emit-sub-group-child` through `em/resolve-template-source` so the legacy fallback that re-derived `stateful-host-for-template` is gone. Replace the `volatile!` in `dnd/drag/marquee-hits` with an `into` + `keep` pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) --- public/index.html | 12 ++ src/bareforge/dnd/drag.cljs | 32 ++--- src/bareforge/doc/sanitize.cljs | 118 ++++++++++++++++++- src/bareforge/export/cljs_project.cljs | 65 +++++----- src/bareforge/export/model.cljs | 33 ++++++ src/bareforge/export/vanilla_js/codegen.cljs | 15 +-- src/bareforge/main.cljs | 29 +++-- src/bareforge/storage/indexeddb.cljs | 64 +++++++--- src/bareforge/storage/project_file.cljs | 34 +++++- src/bareforge/ui/toolbar.cljs | 41 +++++-- test/bareforge/doc/sanitize_test.cljs | 59 ++++++++++ test/bareforge/export/model_test.cljs | 16 +++ test/bareforge/storage/indexeddb_test.cljs | 13 +- test/bareforge/ui/toolbar_test.cljs | 7 ++ 14 files changed, 429 insertions(+), 109 deletions(-) diff --git a/public/index.html b/public/index.html index 6fe25ac..f373b0d 100644 --- a/public/index.html +++ b/public/index.html @@ -93,6 +93,18 @@ background: var(--x-color-border, rgba(127, 127, 127, 0.35)); } .toolbar-icon-button span { font-size: 16px; line-height: 1; } + .toolbar-autosave-warn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + margin-left: 6px; + color: var(--x-color-warning, #c97a00); + font-size: 16px; + cursor: help; + } + .toolbar-autosave-warn[hidden] { display: none; } .panel { border-right: 1px solid var(--x-color-border, rgba(127, 127, 127, 0.2)); padding: 12px; diff --git a/src/bareforge/dnd/drag.cljs b/src/bareforge/dnd/drag.cljs index 2248101..a3cae29 100644 --- a/src/bareforge/dnd/drag.cljs +++ b/src/bareforge/dnd/drag.cljs @@ -229,22 +229,22 @@ (let [^js br (.getBoundingClientRect host) sl (.-scrollLeft host) st (.-scrollTop host) - ^js nodes (.querySelectorAll host "[data-bareforge-id]") - out (volatile! [])] - (dotimes [i (.-length nodes)] - (let [^js el (.item nodes i) - id (.getAttribute el "data-bareforge-id")] - (when (and id (not= id "root")) - (let [^js eb (.getBoundingClientRect el) - left (+ (- (.-left eb) (.-left br)) sl) - top (+ (- (.-top eb) (.-top br)) st) - rect {:left left - :top top - :right (+ left (.-width eb)) - :bottom (+ top (.-height eb))}] - (when (rects-overlap? marquee-rect rect) - (vswap! out conj id)))))) - @out)) + ^js nodes (.querySelectorAll host "[data-bareforge-id]")] + (into [] + (keep (fn [i] + (let [^js el (.item nodes i) + id (.getAttribute el "data-bareforge-id")] + (when (and id (not= id "root")) + (let [^js eb (.getBoundingClientRect el) + left (+ (- (.-left eb) (.-left br)) sl) + top (+ (- (.-top eb) (.-top br)) st) + rect {:left left + :top top + :right (+ left (.-width eb)) + :bottom (+ top (.-height eb))}] + (when (rects-overlap? marquee-rect rect) + id)))))) + (range (.-length nodes))))) (defn- commit-marquee! [^js e] (let [^js host (canvas-el) diff --git a/src/bareforge/doc/sanitize.cljs b/src/bareforge/doc/sanitize.cljs index 0131781..6f0d1f4 100644 --- a/src/bareforge/doc/sanitize.cljs +++ b/src/bareforge/doc/sanitize.cljs @@ -2,14 +2,20 @@ "Pure sanitisation helpers for the two doc fields that ship raw user-supplied strings into the DOM: `:inner-html` (raw SVG/HTML on `:raw-html-slot?` components like x-icon) and `:attrs` URL values - (`href`, `src`, …). + (`href`, `src`, …). Plus an identifier-shape scanner that guards + the export pipeline against doc fields that the codegen later + interpolates verbatim into emitted JS / CLJS source. - Two layers of protection: + Three layers of protection: - **Block-list scanners** (`unsafe-findings`): given a doc, return a vector of `[path reason]` entries naming each suspect site. Used at the load boundary by `storage/project-file/validate-project` to refuse a malicious payload outright with an explanatory message. + Covers XSS payloads in `:inner-html` / URL attrs **and** unsafe + identifier shapes in attr keys, binding keys, field / action + names, and trigger action-refs (everything codegen splices into + a JS or CLJS string literal). - **Best-effort sanitisers** (`sanitize-svg-fragment`, `sanitize-doc`): strip the obvious payloads — `