46f93fd6eb
Deploy marketing to Cloud Run / deploy (push) Failing after 5s
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
34 lines
1.6 KiB
SQL
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';
|