From df6d89cb59cc81a365b926ae0d63270bb7dcb9d9 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 27 May 2026 21:17:40 -0400 Subject: [PATCH] =?UTF-8?q?fix(secure-context):=20full=20audit=20=E2=80=94?= =?UTF-8?q?=20DestructiveConfirmModal.expectedTokenOverride=20+=20bulk-del?= =?UTF-8?q?ete=20+=20min-dim=20use=20backend-computed=20tokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator-flagged 2026-05-27: walk the whole project for the same shape as the min-dim Delete-button silent failure (crypto.subtle TypeError on plain HTTP). FC runs over plain HTTP per the homelab posture; Secure-Context-gated browser APIs are undefined on the production origin. **Audit results across `frontend/src/`:** crypto.subtle.digest — 2 sites: - MinDimensionCard (fixed 2026-05-27) - BulkEditorPanel (THIS FIX) navigator.clipboard — 1 site, already guarded: - utils/clipboard.js writeText with execCommand fallback serviceWorker / mediaDevices / Push / Web USB|HID|Bluetooth|Serial / cookieStore / queryLocalFonts / WebAuthn / geolocation — NOT USED, nothing to fix Extension scripts (background.js) use crypto.subtle but run from moz-extension:// which IS a Secure Context — left as-is. **BulkEditorPanel double bug** The bulk-delete UI on the gallery selection had been broken since FC-3k shipped, in two ways: 1. `crypto.subtle.digest` swallowed TypeError on plain HTTP — modal never opened. Same symptom as min-dim. 2. Even on HTTPS, the modal's `kind="images-selection"` produced `delete-images-selection-` while the backend expected `delete-images-`. The two would never match. Fix: - Backend `/api/admin/images/bulk-delete` dry-run response now returns `confirm_token` (the canonical `delete-images-` string). Integration test `test_bulk_delete_dry_run_returns_counts` pinned to assert the new field. - DestructiveConfirmModal gains an `expectedTokenOverride` prop. When set, it bypasses the `${action}-${kind}-${runId}` formula and uses the explicit string. This decouples the UI label (`kind`) from the wire-format token (server-provided), so future endpoints can use a kind-specific label without their kind name leaking into the token. - BulkEditorPanel passes `:expected-token-override="bulkProjected?.confirm_token"` — no client-side crypto, no kind-prefix mismatch. - MinDimensionCard refactored to the same explicit pattern (was slicing the 8-char suffix off the backend's token and passing it through `runId`; now passes the full backend token via `expected-token-override` directly). Cleaner; one source of truth. **Banked memory** `feedback_no_secure_context_apis.md` documents the full table of Secure-Context-gated APIs, which ones FC currently uses, and how each is handled. Indexed in MEMORY.md. Sites for the audit also listed in the memory for future drift-checking. No other Secure-Context-gated APIs found in `frontend/src/`. The same shape won't recur unless someone adds a new dependency on one — at which point the banked memory should fire. --- backend/app/api/admin.py | 14 +++++++--- .../components/cleanup/MinDimensionCard.vue | 27 +++++++------------ .../components/gallery/BulkEditorPanel.vue | 25 ++++++++--------- .../modal/DestructiveConfirmModal.vue | 14 ++++++++-- tests/test_api_admin.py | 5 ++++ 5 files changed, 49 insertions(+), 36 deletions(-) diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index cefc181..fd3c5ce 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -97,11 +97,19 @@ async def images_bulk_delete(): ) ) - if dry_run: - return jsonify(projected) - sha8 = _bulk_image_confirm_token(image_ids) expected = f"delete-images-{sha8}" + + if dry_run: + # Hand the canonical Tier-C confirm token back with the + # projection so the frontend doesn't have to recompute SHA-256 + # client-side via crypto.subtle (Secure-Context-gated, + # undefined on plain-HTTP origins per the homelab posture). + # Operator-flagged 2026-05-27. + projected["confirm_token"] = expected + return jsonify(projected) + + if supplied_confirm != expected: return _bad( "confirm_mismatch", diff --git a/frontend/src/components/cleanup/MinDimensionCard.vue b/frontend/src/components/cleanup/MinDimensionCard.vue index b1a1e9b..140d549 100644 --- a/frontend/src/components/cleanup/MinDimensionCard.vue +++ b/frontend/src/components/cleanup/MinDimensionCard.vue @@ -52,8 +52,8 @@ v-model="showModal" action="delete" kind="min-dim" - :run-id="tokenSuffix" tier="C" + :expected-token-override="preview?.confirm_token || ''" :projected-counts="projectedCounts" :description="`Width < ${minW} OR height < ${minH}`" @confirm="onConfirmedDelete" @@ -62,11 +62,19 @@