Skip to content

Commit 190ddba

Browse files
joerdava-h
andauthored
feat: add WithNonce for CSP compatibility (#752)
Co-authored-by: Adrian Hesketh <[email protected]>
1 parent e369eaf commit 190ddba

File tree

13 files changed

+567
-11
lines changed

13 files changed

+567
-11
lines changed

.version

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.2.696
1+
0.2.698

docs/docs/10-security/index.md renamed to docs/docs/10-security/01-injection-attacks.md

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
# Security
2-
3-
## Injection attacks
1+
# Injection attacks
42

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

@@ -87,8 +85,3 @@ css className() {
8785
color: { red };
8886
}
8987
```
90-
91-
## Code signing
92-
93-
Binaries are created by https://github.com/a-h and signed with https://adrianhesketh.com/a-h.gpg
94-
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Content security policy
2+
3+
## Nonces
4+
5+
In templ [script templates](/syntax-and-usage/script-templates#script-templates) are rendered as inline `<script>` tags.
6+
7+
Strict Content Security Policies (CSP) can prevent these inline scripts from executing.
8+
9+
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.
10+
11+
:::info
12+
It's your responsibility to generate a secure nonce. Nonces should be generated using a cryptographically secure random number generator.
13+
14+
See https://content-security-policy.com/nonce/ for more information.
15+
:::
16+
17+
## Setting a nonce
18+
19+
The `templ.WithNonce` function can be used to set a nonce for templ to use when rendering scripts.
20+
21+
It returns an updated `context.Context` with the nonce set.
22+
23+
In this example, the `alert` function is rendered as a script element by templ.
24+
25+
```templ title="templates.templ"
26+
package main
27+
28+
import "context"
29+
import "os"
30+
31+
script onLoad() {
32+
alert("Hello, world!")
33+
}
34+
35+
templ template() {
36+
@onLoad()
37+
}
38+
```
39+
40+
```go title="main.go"
41+
package main
42+
43+
import (
44+
"fmt"
45+
"log"
46+
"net/http"
47+
"time"
48+
)
49+
50+
func withNonce(next http.Handler) http.Handler {
51+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
52+
nonce := securelyGenerateRandomString()
53+
w.Header().Add("Content-Security-Policy", fmt.Sprintf("script-src 'nonce-%s'", nonce))
54+
// Use the context to pass the nonce to the handler.
55+
ctx := templ.WithNonce(r.Context(), nonce)
56+
next.ServeHTTP(w, r.WithContext(ctx))
57+
})
58+
}
59+
60+
func main() {
61+
mux := http.NewServeMux()
62+
63+
// Handle template.
64+
mux.HandleFunc("/", templ.Handler(template()))
65+
66+
// Apply middleware.
67+
withNonceMux := withNonce(mux)
68+
69+
// Start the server.
70+
fmt.Println("listening on :8080")
71+
if err := http.ListenAndServe(":8080", withNonceMux); err != nil {
72+
log.Printf("error listening: %v", err)
73+
}
74+
}
75+
```
76+
77+
```html title="Output"
78+
<script type="text/javascript" nonce="randomly generated nonce">
79+
function __templ_onLoad_5a85() {
80+
alert("Hello, world!")
81+
}
82+
</script>
83+
<script type="text/javascript" nonce="randomly generated nonce">
84+
__templ_onLoad_5a85()
85+
</script>
86+
```
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Code signing
2+
3+
Binaries are created by the Github Actions workflow at https://github.com/a-h/templ/blob/main/.github/workflows/release.yml
4+
5+
Binaries are signed by cosign. The public key is stored in the repository at https://github.com/a-h/templ/blob/main/cosign.pub
6+
7+
Instructions for key verification at https://docs.sigstore.dev/verifying/verify/
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package main
2+
3+
import (
4+
"crypto/rand"
5+
"fmt"
6+
"math/big"
7+
"net/http"
8+
"os"
9+
10+
"log/slog"
11+
12+
"github.com/a-h/templ"
13+
)
14+
15+
func main() {
16+
log := slog.New(slog.NewJSONHandler(os.Stdout, nil))
17+
18+
// Create HTTP routes.
19+
mux := http.NewServeMux()
20+
mux.Handle("/", templ.Handler(template()))
21+
22+
// Wrap the router with CSP middleware to apply the CSP nonce to templ scripts.
23+
withCSPMiddleware := NewCSPMiddleware(log, mux)
24+
25+
log.Info("Listening...", slog.String("addr", "127.0.0.1:7001"))
26+
if err := http.ListenAndServe("127.0.0.1:7001", withCSPMiddleware); err != nil {
27+
log.Error("failed to start server", slog.Any("error", err))
28+
}
29+
}
30+
31+
func NewCSPMiddleware(log *slog.Logger, next http.Handler) *CSPMiddleware {
32+
return &CSPMiddleware{
33+
Log: log,
34+
Next: next,
35+
Size: 28,
36+
}
37+
}
38+
39+
type CSPMiddleware struct {
40+
Log *slog.Logger
41+
Next http.Handler
42+
Size int
43+
}
44+
45+
func (m *CSPMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
46+
nonce, err := m.generateNonce()
47+
if err != nil {
48+
m.Log.Error("failed to generate nonce", slog.Any("error", err))
49+
http.Error(w, "Internal server error", http.StatusInternalServerError)
50+
}
51+
ctx := templ.WithNonce(r.Context(), nonce)
52+
w.Header().Add("Content-Security-Policy", fmt.Sprintf("script-src 'nonce-%s'", nonce))
53+
m.Next.ServeHTTP(w, r.WithContext(ctx))
54+
}
55+
56+
func (m *CSPMiddleware) generateNonce() (string, error) {
57+
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
58+
ret := make([]byte, m.Size)
59+
for i := 0; i < m.Size; i++ {
60+
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
61+
if err != nil {
62+
return "", err
63+
}
64+
ret[i] = letters[num.Int64()]
65+
}
66+
return string(ret), nil
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package main
2+
3+
script sayHello() {
4+
alert("Hello")
5+
}
6+
7+
templ template() {
8+
@sayHello()
9+
}

examples/content-security-policy/templates_templ.go

+44
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script type="text/javascript" nonce="nonce1">
2+
function __templ_withParameters_1056(a, b, c){console.log(a, b, c);
3+
}function __templ_withoutParameters_6bbf(){alert("hello");
4+
}
5+
</script>
6+
<button onClick="__templ_withParameters_1056(&#34;test&#34;,&#34;A&#34;,123)" onMouseover="__templ_withoutParameters_6bbf()" type="button">A</button>
7+
<button onClick="__templ_withParameters_1056(&#34;test&#34;,&#34;B&#34;,123)" onMouseover="__templ_withoutParameters_6bbf()" type="button">B</button>
8+
<button onMouseover="console.log(&#39;mouseover&#39;)" type="button">Button C</button>
9+
<button hx-on::click="alert('clicked inline')" type="button">Button D</button>
10+
<script type="text/javascript" nonce="nonce1">
11+
function __templ_onClick_657d(){alert("clicked");
12+
}
13+
</script>
14+
<button hx-on::click="__templ_onClick_657d()" type="button">Button E</button>
15+
<script type="text/javascript" nonce="nonce1">
16+
function __templ_conditionalScript_de41(){alert("conditional");
17+
}
18+
</script>
19+
<input type="button" value="Click me" onclick="__templ_conditionalScript_de41()" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package testscriptusage
2+
3+
import (
4+
"context"
5+
_ "embed"
6+
"testing"
7+
8+
"github.com/a-h/templ"
9+
"github.com/a-h/templ/generator/htmldiff"
10+
)
11+
12+
//go:embed expected.html
13+
var expected string
14+
15+
func Test(t *testing.T) {
16+
component := ThreeButtons()
17+
18+
ctx := templ.WithNonce(context.Background(), "nonce1")
19+
diff, err := htmldiff.DiffCtx(ctx, component, expected)
20+
if err != nil {
21+
t.Fatal(err)
22+
}
23+
if diff != "" {
24+
t.Error(diff)
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package testscriptusage
2+
3+
script withParameters(a string, b string, c int) {
4+
console.log(a, b, c);
5+
}
6+
7+
script withoutParameters() {
8+
alert("hello");
9+
}
10+
11+
script onClick() {
12+
alert("clicked");
13+
}
14+
15+
templ Button(text string) {
16+
<button onClick={ withParameters("test", text, 123) } onMouseover={ withoutParameters() } type="button">{ text }</button>
17+
}
18+
19+
script withComment() {
20+
//'
21+
}
22+
23+
templ ThreeButtons() {
24+
@Button("A")
25+
@Button("B")
26+
<button onMouseover="console.log('mouseover')" type="button">Button C</button>
27+
<button hx-on::click="alert('clicked inline')" type="button">Button D</button>
28+
<button hx-on::click={ onClick() } type="button">Button E</button>
29+
@Conditional(true)
30+
}
31+
32+
script conditionalScript() {
33+
alert("conditional");
34+
}
35+
36+
templ Conditional(show bool) {
37+
<input
38+
type="button"
39+
value="Click me"
40+
if show {
41+
onclick={ conditionalScript() }
42+
}
43+
/>
44+
}

0 commit comments

Comments
 (0)