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{} +}