From 51c39fe8e87ae4cf507dda72a1eefb889a6cb33c Mon Sep 17 00:00:00 2001 From: Andriy Tyurnikov Date: Wed, 29 Apr 2026 16:30:42 +0300 Subject: [PATCH] Make patch/script API safe by default with unsafe-* escape hatches The five user-facing functions now validate / escape values that get written raw onto the SSE wire or into a `")))) + (specify "throws on ")))) + (specify "escapes & before quotes (no double-escape)" + (expect (= """ (d*/escape-script-attribute-value """)))))) + + +(defdescribe test-assert-script-attribute-name-safe! + (describe d*/assert-script-attribute-name-safe! + (specify "accepts valid names" + (expect (= "type" (d*/assert-script-attribute-name-safe! "type"))) + (expect (= "data-x" (d*/assert-script-attribute-name-safe! :data-x))) + (expect (= "x:y" (d*/assert-script-attribute-name-safe! "x:y")))) + (specify "throws on names with whitespace or =\"" + (expect (threw? #(d*/assert-script-attribute-name-safe! "x onclick=foo"))) + (expect (threw? #(d*/assert-script-attribute-name-safe! "x\"y")))) + (specify "throws on empty" + (expect (threw? #(d*/assert-script-attribute-name-safe! "")))))) + + +(comment + (ltr/run-test-var #'test-assert-sse-line-safe!) + (ltr/run-test-var #'test-assert-script-body-safe!) + (ltr/run-test-var #'test-escape-script-attribute-value) + (ltr/run-test-var #'test-assert-script-attribute-name-safe!)) + + +;; ----------------------------------------------------------------------------- +;; Safe-by-default behavior +;; ----------------------------------------------------------------------------- +;; The five public patch/script functions validate their option-line and +;; script inputs by default. The `unsafe-*` twins skip the validation. +(defdescribe test-safe-by-default-rejects-injection + (describe "patch-elements! rejects newlines in option lines" + (specify "id" + (expect (threw? #(d*/patch-elements! (at/->sse-gen) "x" {d*/id "1\nevent: bad"})))) + (specify "selector" + (expect (threw? #(d*/patch-elements! (at/->sse-gen) "x" {d*/selector "x\nevent: bad"})))) + (specify "patch-mode" + (expect (threw? #(d*/patch-elements! (at/->sse-gen) "x" {d*/patch-mode "after\nevent: bad"})))) + (specify "element-ns" + (expect (threw? #(d*/patch-elements! (at/->sse-gen) "x" {d*/element-ns "svg\nevent: bad"}))))) + + (describe "patch-elements-seq! rejects newlines" + (specify "selector" + (expect (threw? #(d*/patch-elements-seq! (at/->sse-gen) ["x"] {d*/selector "x\ny"}))))) + + (describe "remove-element! rejects newlines" + (specify "selector positional arg" + (expect (threw? #(d*/remove-element! (at/->sse-gen) "#x\nevent: bad")))) + (specify "id" + (expect (threw? #(d*/remove-element! (at/->sse-gen) "#x" {d*/id "1\nbad"}))))) + + (describe "patch-signals! rejects newlines" + (specify "id" + (expect (threw? #(d*/patch-signals! (at/->sse-gen) "{}" {d*/id "1\nbad"}))))) + + (describe "execute-script! defends the script tag" + (specify "rejects sse-gen) "")))) + (specify "rejects sse-gen) "x; sse-gen) "x" + {d*/auto-remove false + d*/attributes {"data-x" "a\" data-evil=\"x"}}) + (script-event + "")))) + (specify "rejects malformed attribute names" + (expect (threw? #(d*/execute-script! (at/->sse-gen) "x" + {d*/attributes {"x onclick=foo" "1"}})))))) + + +(defdescribe test-unsafe-variants-skip-validation + (describe "unsafe-* twins do not validate" + (specify "unsafe-patch-elements! lets the injected line through" + (let [out (d*/unsafe-patch-elements! (at/->sse-gen) "x" {d*/id "1\nevent: bad"})] + (expect (.contains ^String out "id: 1\nevent: bad")))) + + (specify "unsafe-patch-signals! lets the injected line through" + (let [out (d*/unsafe-patch-signals! (at/->sse-gen) "{}" {d*/id "1\nbad"})] + (expect (.contains ^String out "id: 1\nbad")))) + + (specify "unsafe-remove-element! lets the injected selector through" + (let [out (d*/unsafe-remove-element! (at/->sse-gen) "#x\nevent: bad")] + (expect (.contains ^String out "selector #x\nevent: bad")))) + + (specify "unsafe-execute-script! does not escape attribute values" + (let [out (d*/unsafe-execute-script! (at/->sse-gen) "alert(1)" + {d*/auto-remove false + d*/attributes {"data-x" "a\" b"}})] + (expect (.contains ^String out "data-x=\"a\" b\"")))))) + + +(comment + (ltr/run-test-var #'test-safe-by-default-rejects-injection) + (ltr/run-test-var #'test-unsafe-variants-skip-validation))