-
-
Notifications
You must be signed in to change notification settings - Fork 280
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add WithNonce for CSP compatibility (#752)
Co-authored-by: Adrian Hesketh <adrianhesketh@hushmail.com>
- Loading branch information
Showing
13 changed files
with
567 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
0.2.699 | ||
0.2.699 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package main | ||
|
||
script sayHello() { | ||
alert("Hello") | ||
} | ||
|
||
templ template() { | ||
@sayHello() | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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("test","A",123)" onMouseover="__templ_withoutParameters_6bbf()" type="button">A</button> | ||
<button onClick="__templ_withParameters_1056("test","B",123)" onMouseover="__templ_withoutParameters_6bbf()" type="button">B</button> | ||
<button onMouseover="console.log('mouseover')" 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()" /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() } | ||
} | ||
/> | ||
} |
Oops, something went wrong.