diff --git a/engine.go b/engine.go
index bcc1295..2f3acc4 100644
--- a/engine.go
+++ b/engine.go
@@ -126,3 +126,11 @@ func (e *Engine) ParseTemplateAndCache(source []byte, path string, line int) (*T
e.cfg.Cache[path] = source
return t, err
}
+
+// SetAutoEscapeReplacer enables auto-escape functionality where the output of expression blocks ({{ ... }}) is
+// passed though a render.Replacer during rendering, unless it's been marked as safe by applying the 'safe' filter.
+// This filter is automatically registered when this method is called. The filter must be applied last.
+// A replacer is provided for escaping HTML (see render.HtmlEscaper).
+func (e *Engine) SetAutoEscapeReplacer(replacer render.Replacer) {
+ e.cfg.SetAutoEscapeReplacer(replacer)
+}
diff --git a/expressions/filters.go b/expressions/filters.go
index f80ee6d..563712f 100644
--- a/expressions/filters.go
+++ b/expressions/filters.go
@@ -33,6 +33,12 @@ func (e FilterError) Error() string {
type valueFn func(Context) values.Value
+func (c *Config) ensureMapIsCreated() {
+ if c.filters == nil {
+ c.filters = make(map[string]interface{})
+ }
+}
+
// AddFilter adds a filter to the filter dictionary.
func (c *Config) AddFilter(name string, fn interface{}) {
rf := reflect.ValueOf(fn)
@@ -46,12 +52,24 @@ func (c *Config) AddFilter(name string, fn interface{}) {
// case rf.Type().Out(1).Implements(…):
// panic(typeError("a filter's second output must be type error"))
}
- if len(c.filters) == 0 {
- c.filters = make(map[string]interface{})
- }
+ c.ensureMapIsCreated()
c.filters[name] = fn
}
+func (c *Config) AddSafeFilter() {
+ if c.filters["safe"] == nil {
+ c.ensureMapIsCreated()
+ c.filters["safe"] = func(in interface{}) interface{} {
+ if in, alreadySafe := in.(values.SafeValue); alreadySafe {
+ return in
+ }
+ return values.SafeValue{
+ Value: in,
+ }
+ }
+ }
+}
+
var closureType = reflect.TypeOf(closure{})
var interfaceType = reflect.TypeOf([]interface{}{}).Elem()
diff --git a/render/autoescape.go b/render/autoescape.go
new file mode 100644
index 0000000..6063eed
--- /dev/null
+++ b/render/autoescape.go
@@ -0,0 +1,21 @@
+package render
+
+import (
+ "io"
+ "strings"
+)
+
+// HtmlEscaper is a Replacer that escapes HTML markup characters. Copied from Go standard library because it's
+// not exposed.
+var HtmlEscaper = strings.NewReplacer(
+ `&`, "&",
+ `'`, "'", // "'" is shorter than "'" and apos was not in HTML until HTML5.
+ `<`, "<",
+ `>`, ">",
+ `"`, """, // """ is shorter than """.
+)
+
+// Replacer interface is used for auto-escape.
+type Replacer interface {
+ WriteString(io.Writer, string) (int, error)
+}
diff --git a/render/autoescape_test.go b/render/autoescape_test.go
new file mode 100644
index 0000000..8a5d5b2
--- /dev/null
+++ b/render/autoescape_test.go
@@ -0,0 +1,74 @@
+package render
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/osteele/liquid/parser"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRenderEscapeFilter(t *testing.T) {
+ cfg := NewConfig()
+ cfg.SetAutoEscapeReplacer(HtmlEscaper)
+ buf := new(bytes.Buffer)
+
+ f := func(t *testing.T, tmpl string, bindings map[string]interface{}, out string) {
+ buf.Reset()
+ root, err := cfg.Compile(tmpl, parser.SourceLoc{})
+ require.NoErrorf(t, err, "")
+ err = Render(root, buf, bindings, cfg)
+ require.NoErrorf(t, err, "")
+ require.Equalf(t, out, buf.String(), "")
+ }
+
+ t.Run("unsafe", func(t *testing.T) {
+ f(t,
+ `{{ input }}`,
+ map[string]interface{}{
+ "input": "",
+ },
+ "<script>doEvilStuff()</script>",
+ )
+ })
+
+ t.Run("safe", func(t *testing.T) {
+ f(t,
+ `{{ input|safe }}`,
+ map[string]interface{}{
+ "input": "",
+ },
+ "",
+ )
+ })
+
+ t.Run("double safe", func(t *testing.T) {
+ f(t,
+ `{{ input|safe|safe }}`,
+ map[string]interface{}{
+ "input": "",
+ },
+ "",
+ )
+ })
+
+ t.Run("unsafe slice result", func(t *testing.T) {
+ f(t,
+ `{{ input }}`,
+ map[string]interface{}{
+ "input": []interface{}{"", ""},
+ },
+ "<a><b>",
+ )
+ })
+
+ t.Run("safe slice result", func(t *testing.T) {
+ f(t,
+ `{{ input|safe }}`,
+ map[string]interface{}{
+ "input": []interface{}{"", ""},
+ },
+ "",
+ )
+ })
+}
diff --git a/render/config.go b/render/config.go
index 65ec55b..5748f97 100644
--- a/render/config.go
+++ b/render/config.go
@@ -9,6 +9,8 @@ type Config struct {
parser.Config
grammar
Cache map[string][]byte
+
+ escapeReplacer Replacer
}
type grammar struct {
@@ -24,3 +26,8 @@ func NewConfig() Config {
}
return Config{Config: parser.NewConfig(g), grammar: g, Cache: map[string][]byte{}}
}
+
+func (c *Config) SetAutoEscapeReplacer(replacer Replacer) {
+ c.escapeReplacer = replacer
+ c.AddSafeFilter()
+}
diff --git a/render/render.go b/render/render.go
index 3e8b812..7f708d3 100644
--- a/render/render.go
+++ b/render/render.go
@@ -66,8 +66,22 @@ func (n *ObjectNode) render(w *trimWriter, ctx nodeContext) Error {
if err != nil {
return wrapRenderError(err, n)
}
- if err := wrapRenderError(writeObject(w, value), n); err != nil {
- return err
+ if sv, isSafe := value.(values.SafeValue); isSafe {
+ err = writeObject(w, sv.Value)
+ } else {
+ var fw io.Writer
+ if replacer := ctx.config.escapeReplacer; replacer != nil {
+ fw = &replacerWriter{
+ replacer: replacer,
+ w: w,
+ }
+ } else {
+ fw = w
+ }
+ err = writeObject(fw, value)
+ }
+ if err != nil {
+ return wrapRenderError(err, n)
}
w.TrimRight(n.TrimRight)
return nil
@@ -129,3 +143,16 @@ func writeObject(w io.Writer, value interface{}) error {
return err
}
}
+
+type replacerWriter struct {
+ replacer Replacer
+ w io.Writer
+}
+
+func (h *replacerWriter) Write(p []byte) (n int, err error) {
+ return h.WriteString(string(p))
+}
+
+func (h *replacerWriter) WriteString(s string) (n int, err error) {
+ return h.replacer.WriteString(h.w, s)
+}
diff --git a/values/value.go b/values/value.go
index 6bee0be..b181917 100644
--- a/values/value.go
+++ b/values/value.go
@@ -222,3 +222,9 @@ func (sv stringValue) PropertyValue(iv Value) Value {
}
return nilValue
}
+
+// SafeValue is a wrapped interface{} to mark it as being safe so that auto-escape is not applied.
+// It is used by the 'safe' filter which is added when (*Engine).SetAutoEscapeReplacer() is called.
+type SafeValue struct {
+ Value interface{}
+}