From 352eb24c3028c0123ad88cc36f999e0ba117ae7b Mon Sep 17 00:00:00 2001 From: Seednode Date: Tue, 30 Jan 2024 13:07:59 -0600 Subject: [PATCH] Add initial CSP header support (currently only for images and error pageS) --- cmd/errors.go | 14 ++++++++++++-- cmd/favicons.go | 11 ++++++----- cmd/refresh.go | 5 +++-- cmd/root.go | 2 +- cmd/web.go | 17 ++++++++++------- types/audio/audio.go | 12 +++++++++--- types/code/code.go | 9 +++++++-- types/flash/flash.go | 11 ++++++++--- types/images/images.go | 26 +++++++++++++++++++++----- types/text/text.go | 9 +++++++-- types/types.go | 18 ++++++++++++++++-- types/video/video.go | 9 +++++++-- 12 files changed, 107 insertions(+), 36 deletions(-) diff --git a/cmd/errors.go b/cmd/errors.go index 78ebb4f..5f48dc2 100644 --- a/cmd/errors.go +++ b/cmd/errors.go @@ -12,6 +12,7 @@ import ( "time" "github.com/yosssi/gohtml" + "seedno.de/seednode/roulette/types" ) var ( @@ -30,7 +31,11 @@ func notFound(w http.ResponseWriter, r *http.Request, path string) error { w.WriteHeader(http.StatusNotFound) w.Header().Add("Content-Type", "text/html") - _, err := io.WriteString(w, gohtml.Format(newPage("Not Found", "404 Page not found"))) + nonce := types.GetNonce(6) + + w.Header().Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' 'nonce-%s';", nonce)) + + _, err := io.WriteString(w, gohtml.Format(newPage("Not Found", "404 Page not found", nonce))) if err != nil { return err } @@ -49,9 +54,14 @@ func notFound(w http.ResponseWriter, r *http.Request, path string) error { func serverError(w http.ResponseWriter, r *http.Request, i interface{}) { startTime := time.Now() + w.WriteHeader(http.StatusInternalServerError) w.Header().Add("Content-Type", "text/html") - io.WriteString(w, gohtml.Format(newPage("Server Error", "An error has occurred. Please try again."))) + nonce := types.GetNonce(6) + + w.Header().Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' 'nonce-%s';", nonce)) + + io.WriteString(w, gohtml.Format(newPage("Server Error", "An error has occurred. Please try again.", nonce))) if Verbose { fmt.Printf("%s | ERROR: Invalid request for %s from %s\n", diff --git a/cmd/favicons.go b/cmd/favicons.go index dc8e412..341be9f 100644 --- a/cmd/favicons.go +++ b/cmd/favicons.go @@ -6,6 +6,7 @@ package cmd import ( "embed" + "fmt" "net/http" "strconv" "strings" @@ -16,15 +17,15 @@ import ( //go:embed favicons/* var favicons embed.FS -const ( - faviconHtml string = ` +func getFavicon(nonce string) string { + return fmt.Sprintf(` - + - ` -) + `, nonce) +} func serveFavicons(errorChannel chan<- error) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { diff --git a/cmd/refresh.go b/cmd/refresh.go index 6cbb3ac..8c686da 100644 --- a/cmd/refresh.go +++ b/cmd/refresh.go @@ -26,10 +26,11 @@ func refreshInterval(r *http.Request) (int64, string) { } } -func refreshFunction(rootUrl string, refreshTimer int64) string { +func refreshFunction(rootUrl string, refreshTimer int64, nonce string) string { var htmlBody strings.Builder - htmlBody.WriteString(fmt.Sprintf("`, fileUri)) + html.WriteString(fmt.Sprintf(``, nonce, fileUri)) html.WriteString(fmt.Sprintf(`
`, rootUrl)) return html.String(), nil diff --git a/types/images/images.go b/types/images/images.go index 4ab2263..5d96a3a 100644 --- a/types/images/images.go +++ b/types/images/images.go @@ -12,6 +12,7 @@ import ( _ "image/jpeg" _ "image/png" "math/rand" + "net/http" "os" "strings" @@ -30,7 +31,15 @@ type Format struct { Fun bool } -func (t Format) Css() string { +func (t Format) CSP(w http.ResponseWriter) string { + nonce := types.GetNonce(6) + + w.Header().Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' 'nonce-%s';", nonce)) + + return nonce +} + +func (t Format) CSS() string { var css strings.Builder css.WriteString(`html,body{margin:0;padding:0;height:100%;}`) @@ -41,7 +50,7 @@ func (t Format) Css() string { css.WriteString(`a{color:inherit;display:block;height:97%;width:100%;text-decoration:none;}`) } - css.WriteString(`img{margin:auto;display:block;max-width:97%;max-height:97%;`) + css.WriteString(`img{margin:auto;display:block;max-width:97%;max-height:97%;color:transparent;`) css.WriteString(`object-fit:scale-down;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)`) if t.Fun { rotate := rand.Intn(360) @@ -69,19 +78,26 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) dimensions.height), nil } -func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) { +func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) { dimensions, err := ImageDimensions(filePath) if err != nil { return "", err } - return fmt.Sprintf(`Roulette selected: %s`, + var w strings.Builder + + w.WriteString(fmt.Sprintf(`Roulette selected: %s`, rootUrl, + nonce, fileUri, dimensions.width, dimensions.height, mime, - fileName), nil + fileName)) + + w.WriteString(fmt.Sprintf(``, nonce)) + + return w.String(), nil } func (t Format) Extensions() map[string]string { diff --git a/types/text/text.go b/types/text/text.go index a90c914..86023fe 100644 --- a/types/text/text.go +++ b/types/text/text.go @@ -7,6 +7,7 @@ package text import ( "errors" "fmt" + "net/http" "os" "strings" "unicode/utf8" @@ -16,7 +17,11 @@ import ( type Format struct{} -func (t Format) Css() string { +func (t Format) CSP(w http.ResponseWriter) string { + return "" +} + +func (t Format) CSS() string { var css strings.Builder css.WriteString(`html,body{margin:0;padding:0;height:100%;}`) @@ -31,7 +36,7 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) return fmt.Sprintf(`%s`, fileName), nil } -func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) { +func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) { body, err := os.ReadFile(filePath) if err != nil { body = []byte{} diff --git a/types/types.go b/types/types.go index 83bf08f..8b88f1c 100644 --- a/types/types.go +++ b/types/types.go @@ -5,6 +5,9 @@ Copyright © 2024 Seednode package types import ( + "crypto/rand" + "encoding/hex" + "net/http" "path/filepath" "slices" "strings" @@ -17,14 +20,17 @@ type Type interface { // should be displayed inline (e.g. code) or embedded (e.g. images) Type() string + // Adds a CSP header and returns a nonce to be used in generated pages + CSP(http.ResponseWriter) string + // Returns a CSS string used to format the corresponding page - Css() string + CSS() string // Returns an HTML element for the specified file Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) // Returns an HTML <body> element used to display the specified file - Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) + Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) // Returns a map of file extensions to MIME type strings. Extensions() map[string]string @@ -129,3 +135,11 @@ func removeDuplicateStr(strSlice []string) []string { } return list } + +func GetNonce(length int) string { + b := make([]byte, length) + if _, err := rand.Read(b); err != nil { + return "" + } + return hex.EncodeToString(b) +} diff --git a/types/video/video.go b/types/video/video.go index 094a7f3..c69d70c 100644 --- a/types/video/video.go +++ b/types/video/video.go @@ -6,6 +6,7 @@ package video import ( "fmt" + "net/http" "path/filepath" "strings" @@ -14,7 +15,11 @@ import ( type Format struct{} -func (t Format) Css() string { +func (t Format) CSP(w http.ResponseWriter) string { + return "" +} + +func (t Format) CSS() string { var css strings.Builder css.WriteString(`html,body{margin:0;padding:0;height:100%;}`) @@ -29,7 +34,7 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) return fmt.Sprintf(`<title>%s`, fileName), nil } -func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) { +func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) { return fmt.Sprintf(``, rootUrl, fileUri,