mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 16:02:19 +00:00
924f9c7e8d
draft.value is a json column (not jsonb), so a client could store a U+0000 escape in it. Any later text extraction (`->>` / `to_jsonb`) on such a value raises 22P05 "unsupported Unicode escape sequence" — one poisoned draft 500'd the whole GET /drafts/list, silently hiding the home-page "This workspace has N drafts" banner (and breaking the global drafts page). Prevent it at the source: sanitize the value in update_draft (the only path that writes client-supplied draft content) so a NUL never reaches the column. strip_json_nul does a single backslash-parity-aware byte pass that removes real NUL escapes (values and keys alike) while leaving a legitimate escaped backslash intact — O(n) with no serde_json::Value tree to allocate, important because the slow path is also hit by any value legitimately containing the text after a backslash (e.g. script source). The clean path is a single substring check. A SQL migration scrubs rows written before this, gated to genuinely-poisoned rows (a real NUL makes value::jsonb raise, distinguishing it from a legitimately escaped backslash). With the data clean, no read-side query needs to change. Tests: unit tests for the strip helper (escaped-backslash no-op, real+literal collision, odd-backslash-run parity, nested keys/values) and an integration test that POSTs a NUL-bearing draft and asserts it is stored and listed NUL-free (fails without the strip). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
31 lines
1.3 KiB
SQL
31 lines
1.3 KiB
SQL
-- One-time cleanup of drafts whose `json` value carries a real U+0000 (NUL)
|
|
-- escape — storable only because `draft.value` is `json`, not `jsonb`. Such a
|
|
-- value makes any `->>`/`to_jsonb` extraction raise `22P05`, which 500'd
|
|
-- GET /drafts/list. New writes are sanitized in the application layer
|
|
-- (update_draft → strip_json_nul); this fixes rows written before that landed.
|
|
--
|
|
-- Only genuinely-poisoned rows are touched: a real NUL makes `value::jsonb`
|
|
-- raise, which distinguishes it from a legitimately escaped backslash sequence
|
|
-- (which `jsonb` accepts). The text replace handles the real-world shape — a NUL
|
|
-- inside a text field. A contrived value where stripping the escape leaves
|
|
-- invalid JSON is left as-is (and can no longer be created).
|
|
DO $$
|
|
DECLARE r RECORD;
|
|
BEGIN
|
|
FOR r IN
|
|
SELECT id, value FROM draft WHERE position(E'\\u0000' in value::text) > 0
|
|
LOOP
|
|
BEGIN
|
|
PERFORM r.value::jsonb; -- not poisoned (legit escaped backslash): skip
|
|
EXCEPTION WHEN others THEN
|
|
BEGIN
|
|
UPDATE draft
|
|
SET value = replace(r.value::text, E'\\u0000', '')::json
|
|
WHERE id = r.id;
|
|
EXCEPTION WHEN others THEN
|
|
NULL; -- pathological shape; cannot strip in SQL, no longer creatable
|
|
END;
|
|
END;
|
|
END LOOP;
|
|
END $$;
|