Skip to content

Commit

Permalink
feat: add WithNonce for CSP compatibility (#752)
Browse files Browse the repository at this point in the history
Co-authored-by: Adrian Hesketh <adrianhesketh@hushmail.com>
  • Loading branch information
joerdav and a-h committed May 21, 2024
1 parent 0239f70 commit 8665d5e
Show file tree
Hide file tree
Showing 13 changed files with 567 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.699
0.2.699
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# Security

## Injection attacks
# Injection attacks

templ is designed to prevent user-provided data from being used to inject vulnerabilities.

Expand Down Expand Up @@ -87,8 +85,3 @@ css className() {
color: { red };
}
```

## Code signing

Binaries are created by https://github.com/a-h and signed with https://adrianhesketh.com/a-h.gpg

86 changes: 86 additions & 0 deletions docs/docs/10-security/02-content-security-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Content security policy

## Nonces

In templ [script templates](/syntax-and-usage/script-templates#script-templates) are rendered as inline `<script>` tags.

Strict Content Security Policies (CSP) can prevent these inline scripts from executing.

By setting a nonce attribute on the `<script>` tag, and setting the same nonce in the CSP header, the browser will allow the script to execute.

:::info
It's your responsibility to generate a secure nonce. Nonces should be generated using a cryptographically secure random number generator.

See https://content-security-policy.com/nonce/ for more information.
:::

## Setting a nonce

The `templ.WithNonce` function can be used to set a nonce for templ to use when rendering scripts.

It returns an updated `context.Context` with the nonce set.

In this example, the `alert` function is rendered as a script element by templ.

```templ title="templates.templ"
package main
import "context"
import "os"
script onLoad() {
alert("Hello, world!")
}
templ template() {
@onLoad()
}
```

```go title="main.go"
package main

import (
"fmt"
"log"
"net/http"
"time"
)

func withNonce(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nonce := securelyGenerateRandomString()
w.Header().Add("Content-Security-Policy", fmt.Sprintf("script-src 'nonce-%s'", nonce))
// Use the context to pass the nonce to the handler.
ctx := templ.WithNonce(r.Context(), nonce)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

func main() {
mux := http.NewServeMux()

// Handle template.
mux.HandleFunc("/", templ.Handler(template()))

// Apply middleware.
withNonceMux := withNonce(mux)

// Start the server.
fmt.Println("listening on :8080")
if err := http.ListenAndServe(":8080", withNonceMux); err != nil {
log.Printf("error listening: %v", err)
}
}
```

```html title="Output"
<script type="text/javascript" nonce="randomly generated nonce">
function __templ_onLoad_5a85() {
alert("Hello, world!")
}
</script>
<script type="text/javascript" nonce="randomly generated nonce">
__templ_onLoad_5a85()
</script>
```
7 changes: 7 additions & 0 deletions docs/docs/10-security/03-code-signing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Code signing

Binaries are created by the Github Actions workflow at https://github.com/a-h/templ/blob/main/.github/workflows/release.yml

Binaries are signed by cosign. The public key is stored in the repository at https://github.com/a-h/templ/blob/main/cosign.pub

Instructions for key verification at https://docs.sigstore.dev/verifying/verify/
67 changes: 67 additions & 0 deletions examples/content-security-policy/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package main

import (
"crypto/rand"
"fmt"
"math/big"
"net/http"
"os"

"log/slog"

"github.com/a-h/templ"
)

func main() {
log := slog.New(slog.NewJSONHandler(os.Stdout, nil))

// Create HTTP routes.
mux := http.NewServeMux()
mux.Handle("/", templ.Handler(template()))

// Wrap the router with CSP middleware to apply the CSP nonce to templ scripts.
withCSPMiddleware := NewCSPMiddleware(log, mux)

log.Info("Listening...", slog.String("addr", "127.0.0.1:7001"))
if err := http.ListenAndServe("127.0.0.1:7001", withCSPMiddleware); err != nil {
log.Error("failed to start server", slog.Any("error", err))
}
}

func NewCSPMiddleware(log *slog.Logger, next http.Handler) *CSPMiddleware {
return &CSPMiddleware{
Log: log,
Next: next,
Size: 28,
}
}

type CSPMiddleware struct {
Log *slog.Logger
Next http.Handler
Size int
}

func (m *CSPMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
nonce, err := m.generateNonce()
if err != nil {
m.Log.Error("failed to generate nonce", slog.Any("error", err))
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
ctx := templ.WithNonce(r.Context(), nonce)
w.Header().Add("Content-Security-Policy", fmt.Sprintf("script-src 'nonce-%s'", nonce))
m.Next.ServeHTTP(w, r.WithContext(ctx))
}

func (m *CSPMiddleware) generateNonce() (string, error) {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
ret := make([]byte, m.Size)
for i := 0; i < m.Size; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return "", err
}
ret[i] = letters[num.Int64()]
}
return string(ret), nil
}
9 changes: 9 additions & 0 deletions examples/content-security-policy/templates.templ
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package main

script sayHello() {
alert("Hello")
}

templ template() {
@sayHello()
}
44 changes: 44 additions & 0 deletions examples/content-security-policy/templates_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions generator/test-script-usage-nonce/expected.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script type="text/javascript" nonce="nonce1">
function __templ_withParameters_1056(a, b, c){console.log(a, b, c);
}function __templ_withoutParameters_6bbf(){alert("hello");
}
</script>
<button onClick="__templ_withParameters_1056(&#34;test&#34;,&#34;A&#34;,123)" onMouseover="__templ_withoutParameters_6bbf()" type="button">A</button>
<button onClick="__templ_withParameters_1056(&#34;test&#34;,&#34;B&#34;,123)" onMouseover="__templ_withoutParameters_6bbf()" type="button">B</button>
<button onMouseover="console.log(&#39;mouseover&#39;)" type="button">Button C</button>
<button hx-on::click="alert('clicked inline')" type="button">Button D</button>
<script type="text/javascript" nonce="nonce1">
function __templ_onClick_657d(){alert("clicked");
}
</script>
<button hx-on::click="__templ_onClick_657d()" type="button">Button E</button>
<script type="text/javascript" nonce="nonce1">
function __templ_conditionalScript_de41(){alert("conditional");
}
</script>
<input type="button" value="Click me" onclick="__templ_conditionalScript_de41()" />
26 changes: 26 additions & 0 deletions generator/test-script-usage-nonce/render_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package testscriptusage

import (
"context"
_ "embed"
"testing"

"github.com/a-h/templ"
"github.com/a-h/templ/generator/htmldiff"
)

//go:embed expected.html
var expected string

func Test(t *testing.T) {
component := ThreeButtons()

ctx := templ.WithNonce(context.Background(), "nonce1")
diff, err := htmldiff.DiffCtx(ctx, component, expected)
if err != nil {
t.Fatal(err)
}
if diff != "" {
t.Error(diff)
}
}
44 changes: 44 additions & 0 deletions generator/test-script-usage-nonce/template.templ
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package testscriptusage

script withParameters(a string, b string, c int) {
console.log(a, b, c);
}

script withoutParameters() {
alert("hello");
}

script onClick() {
alert("clicked");
}

templ Button(text string) {
<button onClick={ withParameters("test", text, 123) } onMouseover={ withoutParameters() } type="button">{ text }</button>
}

script withComment() {
//'
}

templ ThreeButtons() {
@Button("A")
@Button("B")
<button onMouseover="console.log('mouseover')" type="button">Button C</button>
<button hx-on::click="alert('clicked inline')" type="button">Button D</button>
<button hx-on::click={ onClick() } type="button">Button E</button>
@Conditional(true)
}

script conditionalScript() {
alert("conditional");
}

templ Conditional(show bool) {
<input
type="button"
value="Click me"
if show {
onclick={ conditionalScript() }
}
/>
}
Loading

0 comments on commit 8665d5e

Please sign in to comment.