Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add WithNonce for CSP compatibility #752

Merged
merged 4 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.696
0.2.698
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
Loading