diff --git a/.version b/.version
index 8d31eb250..7e3525dda 100644
--- a/.version
+++ b/.version
@@ -1 +1 @@
-0.2.699
\ No newline at end of file
+0.2.699
diff --git a/docs/docs/10-security/index.md b/docs/docs/10-security/01-injection-attacks.md
similarity index 94%
rename from docs/docs/10-security/index.md
rename to docs/docs/10-security/01-injection-attacks.md
index c7775e876..9a1f832cf 100644
--- a/docs/docs/10-security/index.md
+++ b/docs/docs/10-security/01-injection-attacks.md
@@ -1,6 +1,4 @@
-# Security
-
-## Injection attacks
+# Injection attacks
templ is designed to prevent user-provided data from being used to inject vulnerabilities.
@@ -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
-
diff --git a/docs/docs/10-security/02-content-security-policy.md b/docs/docs/10-security/02-content-security-policy.md
new file mode 100644
index 000000000..6ba5e0410
--- /dev/null
+++ b/docs/docs/10-security/02-content-security-policy.md
@@ -0,0 +1,86 @@
+# Content security policy
+
+## Nonces
+
+In templ [script templates](/syntax-and-usage/script-templates#script-templates) are rendered as inline `
+
+```
diff --git a/docs/docs/10-security/03-code-signing.md b/docs/docs/10-security/03-code-signing.md
new file mode 100644
index 000000000..473850196
--- /dev/null
+++ b/docs/docs/10-security/03-code-signing.md
@@ -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/
diff --git a/examples/content-security-policy/main.go b/examples/content-security-policy/main.go
new file mode 100644
index 000000000..72c33a410
--- /dev/null
+++ b/examples/content-security-policy/main.go
@@ -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
+}
diff --git a/examples/content-security-policy/templates.templ b/examples/content-security-policy/templates.templ
new file mode 100644
index 000000000..c88808adc
--- /dev/null
+++ b/examples/content-security-policy/templates.templ
@@ -0,0 +1,9 @@
+package main
+
+script sayHello() {
+ alert("Hello")
+}
+
+templ template() {
+ @sayHello()
+}
diff --git a/examples/content-security-policy/templates_templ.go b/examples/content-security-policy/templates_templ.go
new file mode 100644
index 000000000..4d463f6cb
--- /dev/null
+++ b/examples/content-security-policy/templates_templ.go
@@ -0,0 +1,44 @@
+// Code generated by templ - DO NOT EDIT.
+
+package main
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import "context"
+import "io"
+import "bytes"
+
+func sayHello() templ.ComponentScript {
+ return templ.ComponentScript{
+ Name: `__templ_sayHello_6bd3`,
+ Function: `function __templ_sayHello_6bd3(){alert("Hello")
+}`,
+ Call: templ.SafeScript(`__templ_sayHello_6bd3`),
+ CallInline: templ.SafeScriptInline(`__templ_sayHello_6bd3`),
+ }
+}
+
+func template() templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = sayHello().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
diff --git a/generator/test-script-usage-nonce/expected.html b/generator/test-script-usage-nonce/expected.html
new file mode 100644
index 000000000..9f15abc46
--- /dev/null
+++ b/generator/test-script-usage-nonce/expected.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/generator/test-script-usage-nonce/render_test.go b/generator/test-script-usage-nonce/render_test.go
new file mode 100644
index 000000000..48b2da861
--- /dev/null
+++ b/generator/test-script-usage-nonce/render_test.go
@@ -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)
+ }
+}
diff --git a/generator/test-script-usage-nonce/template.templ b/generator/test-script-usage-nonce/template.templ
new file mode 100644
index 000000000..9364a7525
--- /dev/null
+++ b/generator/test-script-usage-nonce/template.templ
@@ -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) {
+
+}
+
+script withComment() {
+ //'
+}
+
+templ ThreeButtons() {
+ @Button("A")
+ @Button("B")
+
+
+
+ @Conditional(true)
+}
+
+script conditionalScript() {
+ alert("conditional");
+}
+
+templ Conditional(show bool) {
+
+}
diff --git a/generator/test-script-usage-nonce/template_templ.go b/generator/test-script-usage-nonce/template_templ.go
new file mode 100644
index 000000000..e30405e4b
--- /dev/null
+++ b/generator/test-script-usage-nonce/template_templ.go
@@ -0,0 +1,219 @@
+// Code generated by templ - DO NOT EDIT.
+
+package testscriptusage
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import "context"
+import "io"
+import "bytes"
+
+func withParameters(a string, b string, c int) templ.ComponentScript {
+ return templ.ComponentScript{
+ Name: `__templ_withParameters_1056`,
+ Function: `function __templ_withParameters_1056(a, b, c){console.log(a, b, c);
+}`,
+ Call: templ.SafeScript(`__templ_withParameters_1056`, a, b, c),
+ CallInline: templ.SafeScriptInline(`__templ_withParameters_1056`, a, b, c),
+ }
+}
+
+func withoutParameters() templ.ComponentScript {
+ return templ.ComponentScript{
+ Name: `__templ_withoutParameters_6bbf`,
+ Function: `function __templ_withoutParameters_6bbf(){alert("hello");
+}`,
+ Call: templ.SafeScript(`__templ_withoutParameters_6bbf`),
+ CallInline: templ.SafeScriptInline(`__templ_withoutParameters_6bbf`),
+ }
+}
+
+func onClick() templ.ComponentScript {
+ return templ.ComponentScript{
+ Name: `__templ_onClick_657d`,
+ Function: `function __templ_onClick_657d(){alert("clicked");
+}`,
+ Call: templ.SafeScript(`__templ_onClick_657d`),
+ CallInline: templ.SafeScriptInline(`__templ_onClick_657d`),
+ }
+}
+
+func Button(text string) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, withParameters("test", text, 123), withoutParameters())
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func withComment() templ.ComponentScript {
+ return templ.ComponentScript{
+ Name: `__templ_withComment_9cf8`,
+ Function: `function __templ_withComment_9cf8(){//'
+}`,
+ Call: templ.SafeScript(`__templ_withComment_9cf8`),
+ CallInline: templ.SafeScriptInline(`__templ_withComment_9cf8`),
+ }
+}
+
+func ThreeButtons() templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var5 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var5 == nil {
+ templ_7745c5c3_Var5 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = Button("A").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = Button("B").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, onClick())
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = Conditional(true).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func conditionalScript() templ.ComponentScript {
+ return templ.ComponentScript{
+ Name: `__templ_conditionalScript_de41`,
+ Function: `function __templ_conditionalScript_de41(){alert("conditional");
+}`,
+ Call: templ.SafeScript(`__templ_conditionalScript_de41`),
+ CallInline: templ.SafeScriptInline(`__templ_conditionalScript_de41`),
+ }
+}
+
+func Conditional(show bool) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var7 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var7 == nil {
+ templ_7745c5c3_Var7 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, conditionalScript())
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
diff --git a/runtime.go b/runtime.go
index ac6952eed..9216edcf2 100644
--- a/runtime.go
+++ b/runtime.go
@@ -41,6 +41,20 @@ func (cf ComponentFunc) Render(ctx context.Context, w io.Writer) error {
return cf(ctx, w)
}
+// WithNonce sets a CSP nonce on the context and returns it.
+func WithNonce(ctx context.Context, nonce string) context.Context {
+ ctx, v := getContext(ctx)
+ v.nonce = nonce
+ return ctx
+}
+
+// GetNonce returns the CSP nonce value set with WithNonce, or an
+// empty string if none has been set.
+func GetNonce(ctx context.Context) (nonce string) {
+ _, v := getContext(ctx)
+ return v.nonce
+}
+
func WithChildren(ctx context.Context, children Component) context.Context {
ctx, v := getContext(ctx)
v.children = &children
@@ -578,6 +592,7 @@ const contextKey = contextKeyType(0)
type contextValue struct {
ss map[string]struct{}
children *Component
+ nonce string
}
func (v *contextValue) setHasOnceBeenRendered(id string) {
@@ -678,13 +693,22 @@ type ComponentScript struct {
var _ Component = ComponentScript{}
+func writeScriptHeader(ctx context.Context, w io.Writer) (err error) {
+ var nonceAttr string
+ if nonce := GetNonce(ctx); nonce != "" {
+ nonceAttr = " nonce=\"" + EscapeString(nonce) + "\""
+ }
+ _, err = fmt.Fprintf(w, `