Files
neuron-web/migrations/20260502185500_backfill_resanitize_share_cards.sql
T
Will Anderson 46f93fd6eb
Deploy marketing to Cloud Run / deploy (push) Failing after 5s
security: replace denylist sanitize_share_html with allowlist el_html_sanitize
A real attacker probed /api/share earlier today with <script>alert(1),
<iframe src=evil>, <img onerror>, <a href="javascript:...">, and a
<form action="/steal"> payload. Nothing executed because the chat
bubble at /share/<id> renders the served HTML inside marked.js's
already-escaped output, but the prior denylist sanitizer was fragile:

  - It comment-wrapped dangerous tags ("<!--script>...-->") which a
    literal "-->" inside an attacker-supplied attribute value can close
    early, re-exposing the original payload.
  - It renamed on*= attributes to data-x-on*= which left attack
    indicators visible in the served HTML.
  - It was a denylist; every new attack vector required a code change.
  - It didn't validate <a href> URL schemes properly.

The replacement is a runtime-level state-machine allowlist parser
(foundation/el af480f6: el_html_sanitize). The product just specifies
the JSON allowlist of allowed tags + attributes; the runtime drops
everything else, validates href/src URL schemes (http/https/mailto/
fragment/relative only), and drops whole subtrees of script/style/
iframe/object/embed/form regardless of the allowlist.

Phase 4 of bl-dc55ae07: deletes sanitize_share_html (main.el) and
gal_sanitize_html (gallery.el); replaces 3 call sites with
el_html_sanitize(html, allowlist). Defines default_share_allowlist
in main.el and the identical gallery_share_allowlist in gallery.el
(separate bindings to avoid a forward-reference at build-concat
order — gallery is concatenated before main).

Phase 5: migrations/20260502185500_backfill_resanitize_share_cards.sql
nulls answer_html for any share_cards row older than 1 hour. Applied
via the Supabase Management API; 0 rows in scope (the column was
added today and existing rows pre-date its first write).

Also fixes an orthogonal duplicate-symbol bug: unix_timestamp() was
defined in both dist/web_stubs.c and the runtime (the latter is a
recent runtime addition picked up by the runtime sync). Removed the
stub.

Backlog: bl-dc55ae07
2026-05-02 12:56:33 -05:00

34 lines
1.6 KiB
SQL

-- 20260502185500_backfill_resanitize_share_cards.sql
--
-- Backfill: NULL out answer_html for share_cards rows older than 1 hour.
--
-- Context: the original sanitize_share_html function was a denylist that
-- comment-wrapped dangerous tags (<!--script>...-->) and renamed on*=
-- attributes to data-x-on*=. A real attacker probed /api/share with
-- <script>, <iframe>, <img onerror>, javascript:, and a <form> payload.
-- Nothing executed, but the comment-wrapping is fragile to a literal
-- "-->" inside an attacker-supplied attribute value, and the renamed
-- attrs left attack indicators visible in served HTML.
--
-- The replacement is a runtime-level allowlist sanitizer (el_html_sanitize)
-- that drops any tag/attribute not on the explicit list. Existing rows
-- that were sanitized by the OLD denylist may carry residual artefacts
-- ("<!--script>alert(1)/script-->" sitting inside answer_html). Rather
-- than rewriting the column row-by-row, we null out answer_html for
-- everything older than 1 hour. The /share/<id> and /said gallery
-- handlers both fall back to the plaintext answer column when answer_html
-- is null, so the user-visible regression is "old shares lose markdown
-- formatting" — not "old shares disappear".
--
-- Anything created in the last hour is post-deploy or pre-deploy testing
-- and is left intact for inspection.
--
-- Applied out-of-band via the Supabase Management API
-- (POST /v1/projects/<ref>/database/query). This file lands the change
-- in version control as the canonical record.
update public.share_cards
set answer_html = null
where answer_html is not null
and created_at < now() - interval '1 hour';