Skip to content

Commit

Permalink
Fix #536: sanitize HTML (#541)
Browse files Browse the repository at this point in the history
  • Loading branch information
borkdude authored Jun 24, 2024
1 parent 043bbd4 commit 1aa8a67
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 31 deletions.
21 changes: 11 additions & 10 deletions src/squint/compiler.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@
(let [f (first expr)]
(or (keyword? f)
(symbol? f))))
(let [top-dynamic-expr (:top-has-dynamic-expr env)
(let [need-html-import (:need-html-import env)
has-dynamic-expr? (or (:has-dynamic-expr env) (atom false))
env (assoc env :has-dynamic-expr has-dynamic-expr?)
v expr
Expand All @@ -307,7 +307,8 @@
tag-name (if (and (not fragment?) keyw?)
(subs (str tag) 1)
(emit tag-name* (expr-env (dissoc env :jsx))))
html? (:html env)]
html? (:html env)
outer-html? (:outer-html (meta expr))]
(if (and (not html?) (:jsx env) (:jsx-runtime env))
(let [single-child? (= 1 (count elts))]
(emit (list (if single-child?
Expand Down Expand Up @@ -344,18 +345,18 @@
(if (and html? fragment?)
""
(str "</" tag-name ">")))]
(when @has-dynamic-expr?
(when top-dynamic-expr
(reset! top-dynamic-expr true)))
(when outer-html?
(when need-html-import
(reset! need-html-import true)))
(emit-return
(cond->> ret
(:outer-html (meta expr))
outer-html?
(format "%s`%s`"
(if-let [t (:tag (meta expr))]
(emit t (expr-env (dissoc env :jsx :html)))
(if @has-dynamic-expr?
"squint_html.tag"
""))))
"squint_html.html"))))
env))))
(emit-return (format "[%s]"
(str/join ", " (emit-args env expr))) env)))
Expand Down Expand Up @@ -475,7 +476,7 @@
cc/*target* :squint
*jsx* false
cc/*repl* (:repl opts cc/*repl*)]
(let [has-dynamic-expr (atom false)
(let [need-html-import (atom false)
opts (merge {:ns-state (atom {})
:top-level true} opts)
imported-vars (atom {})
Expand All @@ -502,7 +503,7 @@
:imports imports
:jsx false
:pragmas pragmas
:top-has-dynamic-expr has-dynamic-expr))
:need-html-import need-html-import))
jsx *jsx*
_ (when (and jsx jsx-runtime)
(swap! imports str
Expand All @@ -516,7 +517,7 @@
(if jsx-dev
"/jsx-dev-runtime"
"/jsx-runtime")))))
_ (when @has-dynamic-expr
_ (when @need-html-import
(swap! imports str
(if cc/*repl*
"var squint_html = await import('squint-cljs/src/squint/html.js');\n"
Expand Down
4 changes: 3 additions & 1 deletion src/squint/compiler_common.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@
"")
(format (if (:html-attr env)
"squint_html.attr(%s)"
"%s")
"%s" #_(if html?
"squint_html._safe(%s)"
"%s"))
expr)))
expr)))

Expand Down
58 changes: 47 additions & 11 deletions src/squint/html.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
import * as squint_core from './core.js';

class Html {
constructor(s) {
// if (typeof(s) !== 'string')
// throw Error(`Object not a string: ${s.constructor}`);
this.s = s;
}
toString() {
return this.s.toString();
}
}

export function html([s]) {
return new Html(s);
}

function escapeHTML(text) {
return text.toString()
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;");
}

function safe(x) {
if (x instanceof Html) return x;
if (squint_core.string_QMARK_(x)) {
return escapeHTML(x);
}
return escapeHTML(x.toString());
}

function css(v) {
let ret = "";
if (v == null) return ret;
Expand All @@ -22,6 +56,17 @@ export function attr(v) {
}
}

function toHTML(v) {
// console.log('v', v);
if (v == null) return;
if (v instanceof Html) return v;
if (typeof(v) === 'string') return safe(v);
if (v[Symbol.iterator]) {
return [...v].map(toHTML).join("");
}
return safe(v.toString());
}

export function attrs(v, props) {
v = Object.assign(props, v);
let ret = "";
Expand All @@ -39,16 +84,7 @@ export function attrs(v, props) {
ret += '"';
first = false;
}
return ret;
}

function toHTML(v) {
if (v == null) return;
if (typeof(v) === 'string') return v;
if (v[Symbol.iterator]) {
return [...v].join("");
}
return v;
return new Html(ret);
}

export function tag(strs, ...vals) {
Expand All @@ -57,5 +93,5 @@ export function tag(strs, ...vals) {
out += toHTML(vals[i]);
out += strs[i+1];
}
return out;
return new Html(out);
}
47 changes: 38 additions & 9 deletions test/squint/html_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
[clojure.test :as t :refer [deftest is]]
[squint.test-utils :refer [jss!]]
[squint.compiler :as squint]
[clojure.string :as str]))
[clojure.string :as str]
[promesa.core :as p]))

(defn html= [x y]
(= (str x) (str y)))

(deftest html-test
(t/async done
Expand All @@ -15,14 +19,14 @@
"foo.bar`<div>Hello</div>"))
(let [{:keys [imports body]} (squint.compiler/compile-string* "(defn foo [x] #html [:div \"Hello\" x])")]
(is (str/includes? imports "import * as squint_html from 'squint-cljs/src/squint/html.js'"))
(is (str/includes? body "squint_html.tag`<div>Hello${x}</div>`")))
(is (str/includes? body "squint_html.tag`<div>Hello${x}</div>")))
(let [js (squint.compiler/compile-string "
(defn li [x] #html [:li x])
(defn foo [x] #html [:ul (map #(li %) (range x))]) (foo 5)" {:repl true :elide-exports true :context :return})
js (str/replace "(async function() { %s } )()" "%s" js)]
(-> (js/eval js)
(.then
#(is (= "<ul><li>0</li><li>1</li><li>2</li><li>3</li><li>4</li></ul>" %)))
#(is (html= "<ul><li>0</li><li>1</li><li>2</li><li>3</li><li>4</li></ul>" %)))
(.catch #(is false "nooooo"))
(.finally done)))))

Expand All @@ -35,7 +39,7 @@
js (str/replace "(async function() { %s } )()" "%s" js)]
(-> (js/eval js)
(.then
#(is (= "<div class=\"foo\" id=\"6\" style=\"color:green;\"></div>" %)))
#(is (html= "<div class=\"foo\" id=\"6\" style=\"color:green;\"></div>" %)))
(.catch #(is false "nooooo"))
(.finally done)))))

Expand All @@ -47,7 +51,7 @@
js (str/replace "(async function() { %s } )()" "%s" js)]
(-> (js/eval js)
(.then
#(is (= "<div>undefined</div>" %)))
#(is (html= "<div>undefined</div>" %)))
(.catch #(is false "nooooo"))
(.finally done)))))

Expand All @@ -59,7 +63,7 @@
js (str/replace "(async function() { %s } )()" "%s" js)]
(-> (js/eval js)
(.then
#(is (= "<div a=\"1\" style=\"color:red;\" b=\"2\">Hello</div>" %)))
#(is (html= "<div a=\"1\" style=\"color:red;\" b=\"2\">Hello</div>" %)))
(.catch #(is false "nooooo"))
(.finally done)))))

Expand All @@ -71,7 +75,7 @@
js (str/replace "(async function() { %s } )()" "%s" js)]
(-> (js/eval js)
(.then
#(is (= "<div style=\"color:green; width:200;\">Hello</div>" %)))
#(is (html= "<div style=\"color:green; width:200;\">Hello</div>" %)))
(.catch #(is false "nooooo"))
(.finally done)))))

Expand All @@ -83,6 +87,31 @@
js (str/replace "(async function() { %s } )()" "%s" js)]
(-> (js/eval js)
(.then
#(is (= "<div>Hello</div>" %)))
(.catch #(is false "nooooo"))
#(is (html= "<div>Hello</div>" %)))
(.catch #(do
(js/console.log %)
(is false "nooooo")))
(.finally done)))))

(defn compile-html [s]
(let [js (squint.compiler/compile-string s
{:repl true :elide-exports true :context :return})
js (str/replace "(async function() { %s } )()" "%s" js)]
js))

(deftest html-safe-test
(t/async done
(->
(p/do
(p/let [js (compile-html
"(defn foo [x] #html [:div x]) (foo \"<>\")")
v (js/eval js)
_ (is (html= "<div>&lt;&gt;</div>" v))])
(p/let [js (compile-html
"(defn foo [x] #html [:div x]) (defn bar [] #html [:div (foo 1)])
(bar)")
v (js/eval js)
_ (is (html= "<div><div>1</div></div>" v))]))
(p/catch #(is false "nooooo"))
(p/finally done))))

14 changes: 14 additions & 0 deletions test/squint/test_utils.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@
[clojure.test :as t]
[squint.compiler :as squint]))

(defn testing-vars-str
"Returns a string representation of the current test. Renders names
in *testing-vars* as a list, then the source file and line of
current assertion."
[m]
(let [{:keys [file line column]} m]
(str
(reverse (map #(:name (meta %)) (:testing-vars (t/get-current-env))))
" (" file ":" line (when column (str ":" column)) ")")))

(defmethod cljs.test/report [:cljs.test/default :begin-test-var] [m]
(println "===" (-> m testing-vars-str))
(println))

(doseq [k (js/Object.keys cl)]
(aset js/globalThis k (aget cl k)))

Expand Down

0 comments on commit 1aa8a67

Please sign in to comment.