Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ To view more specific examples, you could visit each engine folder to learn more
- [pug](./pug/README.md)
- [slim](./slim/README.md)


### embedded Systems

We support the `http.FileSystem` interface, so you can use different libraries to load the templates from embedded binaries.
Expand Down
9 changes: 8 additions & 1 deletion ace/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,14 @@ func main() {
}, "layouts/main")
})

log.Fatal(app.Listen(":3000"))
log.Fatal(app.Listen(":3000"))
}

```

## Security

- Ace templates in this repository compile down to Go's `html/template`, so output benefits from contextual escaping by default.
- Layout composition should continue to use `{{embed}}`, which receives already-rendered child output.
- Custom helpers become a trust boundary when they return trusted `html/template` types such as `template.HTML`, `template.JS`, `template.URL`, or `template.CSS`.
- Do not convert user input into trusted `html/template` types unless you have already fully sanitized it for the target context.
50 changes: 50 additions & 0 deletions ace/security_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package ace

import (
"bytes"
"testing"

"github.com/stretchr/testify/require"
)

const xssPayload = `<script>alert(1)</script>`

func Test_XSS(t *testing.T) {
t.Parallel()
engine := New("./views", ".ace")
engine.AddFunc("isAdmin", func(user string) bool {
return user == admin
})
require.NoError(t, engine.Load())

var buf bytes.Buffer
err := engine.Render(&buf, "simple", map[string]interface{}{
"Title": xssPayload,
})
require.NoError(t, err)

result := trim(buf.String())
require.NotContains(t, result, xssPayload)
require.Contains(t, result, "&lt;script&gt;alert(1)&lt;/script&gt;")
}

func Test_Layout_DoesNotTrustUserProvidedEmbed(t *testing.T) {
t.Parallel()
engine := New("./views", ".ace")
engine.AddFunc("isAdmin", func(user string) bool {
return user == admin
})
require.NoError(t, engine.Load())

var buf bytes.Buffer
err := engine.Render(&buf, "index", map[string]interface{}{
"Title": "Hello, World!",
"embed": xssPayload,
}, "layouts/main")
require.NoError(t, err)

result := trim(buf.String())
require.Contains(t, result, "Hello, World!")
require.NotContains(t, result, xssPayload)
require.NotContains(t, result, "&lt;script&gt;alert(1)&lt;/script&gt;")
}
9 changes: 8 additions & 1 deletion amber/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,14 @@ func main() {
}, "layouts/main")
})

log.Fatal(app.Listen(":3000"))
log.Fatal(app.Listen(":3000"))
}

```

## Security

- Amber templates in this repository compile down to Go's `html/template`, so output benefits from contextual escaping by default.
- Layout composition should continue to use `#{embed()}`, which receives already-rendered child output.
- Custom helpers become a trust boundary when they return trusted `html/template` types such as `template.HTML`, `template.JS`, `template.URL`, or `template.CSS`.
- Do not convert user input into trusted `html/template` types unless you have already fully sanitized it for the target context.
14 changes: 12 additions & 2 deletions amber/amber.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import (
"os"
"path/filepath"
"strings"
"sync"

"github.com/eknkc/amber"
core "github.com/gofiber/template/v2"
)

var amberFuncMapMu sync.Mutex

// Engine struct
type Engine struct {
core.Engine
Expand Down Expand Up @@ -67,15 +70,22 @@ func (e *Engine) Load() error {
// prepare the global amber funcs
funcs := template.FuncMap{}

for k, v := range amber.FuncMap { // add the amber's default funcs
amberFuncMapMu.Lock()
previousFuncMap := amber.FuncMap
defer func() {
amber.FuncMap = previousFuncMap //nolint:reassign // restore the prior compiler state after loading templates.
amberFuncMapMu.Unlock()
}()

for k, v := range previousFuncMap { // add the amber's default funcs
funcs[k] = v
}

for k, v := range e.Funcmap {
funcs[k] = v
}

amber.FuncMap = funcs //nolint:reassign // this is fine, as long as it's not run in parallel in a test.
amber.FuncMap = funcs //nolint:reassign // amber compiler reads from this package global during compilation.

// Loop trough each directory and register template files
walkFn := func(path string, info os.FileInfo, err error) error {
Expand Down
50 changes: 50 additions & 0 deletions amber/security_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package amber

import (
"bytes"
"testing"

"github.com/stretchr/testify/require"
)

const xssPayload = `<script>alert(1)</script>`

func Test_XSS(t *testing.T) {
t.Parallel()
engine := New("./views", ".amber")
engine.AddFunc("isAdmin", func(user string) bool {
return user == admin
})
require.NoError(t, engine.Load())

var buf bytes.Buffer
err := engine.Render(&buf, "simple", map[string]interface{}{
"Title": xssPayload,
})
require.NoError(t, err)

result := trim(buf.String())
require.NotContains(t, result, xssPayload)
require.Contains(t, result, "&lt;script&gt;alert(1)&lt;/script&gt;")
}

func Test_Layout_DoesNotTrustUserProvidedEmbed(t *testing.T) {
t.Parallel()
engine := New("./views", ".amber")
engine.AddFunc("isAdmin", func(user string) bool {
return user == admin
})
require.NoError(t, engine.Load())

var buf bytes.Buffer
err := engine.Render(&buf, "index", map[string]interface{}{
"Title": "Hello, World!",
"embed": xssPayload,
}, "layouts/main")
require.NoError(t, err)

result := trim(buf.String())
require.Contains(t, result, "Hello, World!")
require.NotContains(t, result, xssPayload)
require.NotContains(t, result, "&lt;script&gt;alert(1)&lt;/script&gt;")
}
7 changes: 7 additions & 0 deletions django/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,10 @@ engine.SetAutoEscape(false)
Disabling auto-escape should be approached with caution. It can expose your application to XSS attacks, where malicious scripts are injected into web pages. Without auto-escaping, there is a risk of rendering harmful HTML or JavaScript from user-supplied data.

It is advisable to keep auto-escape enabled unless there is a strong reason to disable it. If you do disable it, ensure all user-supplied content is thoroughly sanitized and validated to avoid XSS vulnerabilities.

## Security

- Auto-escaping is enabled by default and should remain enabled for untrusted content.
- `SetAutoEscape(false)`, `{% autoescape off %}`, and `safe`-style constructs disable escaping and should only be used with trusted, pre-sanitized content.
- Layout composition uses `{{embed}}`, but the repository only marks embed output as safe after the child template has already been rendered.
- Custom globals and helper functions are trusted code and should not manufacture safe HTML from untrusted input.
21 changes: 19 additions & 2 deletions django/django.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,20 @@ import (
"path/filepath"
"reflect"
"strings"
"sync"

"github.com/flosch/pongo2/v6"
core "github.com/gofiber/template/v2"
)

var (
// pongo2 stores autoescape in a package-global variable with a default of true.
// Serialize render-time updates so separate engine instances do not race while
// temporarily swapping that shared setting for execution.
pongo2AutoescapeMu sync.Mutex
pongo2CurrentAutoEscape = true
)

// Engine struct
type Engine struct {
core.Engine
Expand Down Expand Up @@ -84,8 +93,6 @@ func (e *Engine) Load() error {
pongoset := pongo2.NewSet("default", pongoloader)
// Set template settings
pongoset.Globals.Update(e.Funcmap)
// Set autoescaping
pongo2.SetAutoescape(e.autoEscape)

// Loop trough each Directory and register template files
walkFn := func(path string, info os.FileInfo, err error) error {
Expand Down Expand Up @@ -240,6 +247,16 @@ func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout
e.Mutex.Lock()
defer e.Mutex.Unlock()

pongo2AutoescapeMu.Lock()
previousAutoEscape := pongo2CurrentAutoEscape
defer func() {
pongo2CurrentAutoEscape = previousAutoEscape
pongo2.SetAutoescape(previousAutoEscape)
pongo2AutoescapeMu.Unlock()
}()
pongo2CurrentAutoEscape = e.autoEscape
pongo2.SetAutoescape(e.autoEscape)

bind := getPongoBinding(binding)
parsed, err := tmpl.Execute(bind)
if err != nil {
Expand Down
82 changes: 82 additions & 0 deletions django/security_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package django

import (
"bytes"
"os"
"testing"

"github.com/stretchr/testify/require"
)

const xssPayload = `<script>alert(1)</script>`

func Test_XSS_DefaultAutoEscape(t *testing.T) {
t.Parallel()

engine := New("./views", ".django")
engine.AddFunc("isAdmin", func(user string) bool {
return user == admin
})
require.NoError(t, engine.Load())

var buf bytes.Buffer
err := engine.Render(&buf, "simple", map[string]interface{}{
"Title": xssPayload,
})
require.NoError(t, err)

result := trim(buf.String())
require.NotContains(t, result, xssPayload)
require.Contains(t, result, "&lt;script&gt;alert(1)&lt;/script&gt;")
}

func Test_Layout_DoesNotTrustUserProvidedEmbed(t *testing.T) {
t.Parallel()

engine := New("./views", ".django")
engine.AddFunc("isAdmin", func(user string) bool {
return user == admin
})
require.NoError(t, engine.Load())

var buf bytes.Buffer
err := engine.Render(&buf, "index", map[string]interface{}{
"Title": "Hello, World!",
"embed": xssPayload,
}, "layouts/main")
require.NoError(t, err)

result := trim(buf.String())
require.Contains(t, result, "Hello, World!")
require.NotContains(t, result, xssPayload)
require.NotContains(t, result, "&lt;script&gt;alert(1)&lt;/script&gt;")
}

func Test_HelperOutputIsEscaped(t *testing.T) {
t.Parallel()

dir, err := os.MkdirTemp(".", "")
require.NoError(t, err)

defer func() {
err := os.RemoveAll(dir)
require.NoError(t, err)
}()

err = os.WriteFile(dir+"/helper.django", []byte(`<p>{{ helper() }}</p>`), 0o600)
require.NoError(t, err)

engine := New(dir, ".django")
engine.AddFunc("helper", func() string {
return xssPayload
})
require.NoError(t, engine.Load())

var buf bytes.Buffer
err = engine.Render(&buf, "helper", nil)
require.NoError(t, err)

result := trim(buf.String())
require.NotContains(t, result, xssPayload)
require.Contains(t, result, "&lt;script&gt;alert(1)&lt;/script&gt;")
}
9 changes: 8 additions & 1 deletion handlebars/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,14 @@ func main() {
}, "layouts/main")
})

log.Fatal(app.Listen(":3000"))
log.Fatal(app.Listen(":3000"))
}

```

## Security

- Handlebars escapes HTML output by default when you use `{{value}}`.
- Triple-stash expressions such as `{{{value}}}` disable escaping and should only be used for trusted, pre-sanitized markup.
- Layout composition should continue to use `{{embed}}`, which is populated from already-rendered child output.
- Helpers should return plain strings by default; returning `raymond.SafeString` should be reserved for trusted markup only.
Loading
Loading