Skip to content

Instantly share code, notes, and snippets.

@cch1
Created June 23, 2021 19:57
Show Gist options
  • Save cch1/8ac1436ff9167476a6933dd7020f09c0 to your computer and use it in GitHub Desktop.
Save cch1/8ac1436ff9167476a6933dd7020f09c0 to your computer and use it in GitHub Desktop.
An example of a simple templating capability that heavily leverages built-in features of clojure.
(ns template
(:require [clojure.edn :as edn]
[clojure.string :as string]))
;; Tagged literals do not need to be limited to being represented by a single string. Here we represent a value with a tuple of `magic` and `value`.
(defn read-magic [[magic value]]
(case magic
:c (string/capitalize value)
:l (string/lower-case value)
:u (string/upper-case value)))
;; CH templates provided two "interpolation" mechanisms: EDN's tagged literals and arbitrary Clojure code execution.
;; 1. EDN tagged literals: by treating the template as an EDN data structure we can introduce a controlled set of tagged literals.
;; The set of tagged literals available is static at run time (introduced in the first arg to edn/read-string) but the reader
;; for each tagged literal can be as rich as we want -including closures (see tag `c).
;; 2. Code evaluation: *IFF* we control the template, we can ensure it is "safe" to evaluate as Clojure code. This opens up
;; a world of complexity that should be carefully wielded. A simple affordance might be to dynamically bind some
;; well-known local symbols like `tenant` or `user` to provide a substitution mechanism. But be careful where these values (the
;; RHS of the binding pairs) comes from: strings read directly from HTTP query params (and possibly manipulated to cast them) are
;; safe because they cannot become s-expressions.
(defn interpolate-template
"Interpolate the edn serialized data `tstring` and evaluate with bindings provided by the `bindings` map"
[tstring environment bindings]
(let [bindings (mapcat identity bindings)
readers {'magic read-magic 'c (fn [x] (str environment "-" x))} ; adding new tags is this easy
template (edn/read-string {:readers readers} tstring)]
(eval `(let [~@bindings] ~template))))
(let [query-params {"tenant" "gra"}
bindings {'user 23
'tenant (keyword (get query-params "tenant" "gri")) ; this kind of binding injection is safe
'z '(prn "Arbitrary code can sneak in as a binding value ... be careful where you source them!")}
tstring "[tenant {:x 25 :user user :magic #magic [:u \"value\"]} (do (prn \"This is execution of arbitrary code in the template!\") 72) #c \"saas\"]"]
(interpolate-template tstring "production" bindings))
@cch1
Copy link
Author

cch1 commented Jun 23, 2021

I used "user" in my example but in the context of authorization that should be applied unconditionally by surrounding code and not conflated with the user-facing templating functionality.

@cch1
Copy link
Author

cch1 commented Jun 23, 2021

Add-ons: eval in a dynamic namespace with a curated set of requires/imports/defs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment