Skip to content

Commit

Permalink
feat(gnoweb): add secure headers by default & timeout configuration (#…
Browse files Browse the repository at this point in the history
…3619)

This PR adds the following secure headers by default `strict=true` to
gnoweb:

```go
func SecureHeadersMiddleware(next http.Handler, strict bool) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Prevent MIME type sniffing by browsers. This ensures that the browser
		// does not interpret files as a different MIME type than declared.
		w.Header().Set("X-Content-Type-Options", "nosniff")

		// Prevent the page from being embedded in an iframe. This mitigates
		// clickjacking attacks by ensuring the page cannot be loaded in a frame.
		w.Header().Set("X-Frame-Options", "DENY")

		// Control the amount of referrer information sent in the Referer header.
		// 'no-referrer' ensures that no referrer information is sent, which
		// enhances privacy and prevents leakage of sensitive URLs.
		w.Header().Set("Referrer-Policy", "no-referrer")

		// In `strict` mode, prevent cross-site resource forgery and enforce https
		if strict {
			// Define a Content Security Policy (CSP) to restrict the sources of
			// scripts, styles, images, and other resources. This helps prevent
			// cross-site scripting (XSS) and other code injection attacks.
			// - 'self' allows resources from the same origin.
			// - '*' allows images from any external source.
			// - data: is not included to prevent inline images (e.g., base64-encoded images).
			w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' *; font-src 'self'")

			// Enforce HTTPS by telling browsers to only access the site over HTTPS
			// for a specified duration (1 year in this case). This also applies to
			// subdomains and allows preloading into the browser's HSTS list.
			w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
		}

		next.ServeHTTP(w, r)
	})
}

```

I've also enforced a timeout on read/write/idle (default to 1 minute).

cc @kristovatlas

---------

Signed-off-by: gfanton <[email protected]>
Co-authored-by: Antoine Eddi <[email protected]>
Co-authored-by: alexiscolin <[email protected]>
Co-authored-by: Morgan <[email protected]>
  • Loading branch information
4 people authored Feb 14, 2025
1 parent 2e49141 commit 9884ba1
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 5 deletions.
64 changes: 61 additions & 3 deletions gno.land/cmd/gnoweb/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,19 @@ type webCfg struct {
bind string
faucetURL string
assetsDir string
timeout time.Duration
analytics bool
json bool
html bool
noStrict bool
verbose bool
}

var defaultWebOptions = webCfg{
chainid: "dev",
remote: "127.0.0.1:26657",
bind: ":8888",
timeout: time.Minute,
}

func main() {
Expand Down Expand Up @@ -127,7 +130,14 @@ func (c *webCfg) RegisterFlags(fs *flag.FlagSet) {
&c.analytics,
"with-analytics",
defaultWebOptions.analytics,
"nable privacy-first analytics",
"enable privacy-first analytics",
)

fs.BoolVar(
&c.noStrict,
"no-strict",
defaultWebOptions.noStrict,
"allow cross-site resource forgery and disable https enforcement",
)

fs.BoolVar(
Expand All @@ -136,6 +146,13 @@ func (c *webCfg) RegisterFlags(fs *flag.FlagSet) {
defaultWebOptions.verbose,
"verbose logging mode",
)

fs.DurationVar(
&c.timeout,
"timeout",
defaultWebOptions.timeout,
"set read/write/idle timeout for server connections",
)
}

func setupWeb(cfg *webCfg, _ []string, io commands.IO) (func() error, error) {
Expand Down Expand Up @@ -179,18 +196,59 @@ func setupWeb(cfg *webCfg, _ []string, io commands.IO) (func() error, error) {

logger.Info("Running", "listener", bindaddr.String())

// Setup security headers
secureHandler := SecureHeadersMiddleware(app, !cfg.noStrict)

// Setup server
server := &http.Server{
Handler: app,
Handler: secureHandler,
Addr: bindaddr.String(),
ReadHeaderTimeout: 60 * time.Second,
ReadTimeout: cfg.timeout, // Time to read the request
WriteTimeout: cfg.timeout, // Time to write the entire response
IdleTimeout: cfg.timeout, // Time to keep idle connections open
ReadHeaderTimeout: time.Minute, // Time to read request headers
}

return func() error {
if err := server.ListenAndServe(); err != nil {
logger.Error("HTTP server stopped", "error", err)
return commands.ExitCodeError(1)
}

return nil
}, nil
}

func SecureHeadersMiddleware(next http.Handler, strict bool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Prevent MIME type sniffing by browsers. This ensures that the browser
// does not interpret files as a different MIME type than declared.
w.Header().Set("X-Content-Type-Options", "nosniff")

// Prevent the page from being embedded in an iframe. This mitigates
// clickjacking attacks by ensuring the page cannot be loaded in a frame.
w.Header().Set("X-Frame-Options", "DENY")

// Control the amount of referrer information sent in the Referer header.
// 'no-referrer' ensures that no referrer information is sent, which
// enhances privacy and prevents leakage of sensitive URLs.
w.Header().Set("Referrer-Policy", "no-referrer")

// In `strict` mode, prevent cross-site ressources forgery and enforce https
if strict {
// Define a Content Security Policy (CSP) to restrict the sources of
// scripts, styles, images, and other resources. This helps prevent
// cross-site scripting (XSS) and other code injection attacks.
// - 'self' allows resources from the same origin.
// - 'data:' allows inline images (e.g., base64-encoded images).
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'")

// Enforce HTTPS by telling browsers to only access the site over HTTPS
// for a specified duration (1 year in this case). This also applies to
// subdomains and allows preloading into the browser's HSTS list.
w.Header().Set("Strict-Transport-Security", "max-age=31536000")
}

next.ServeHTTP(w, r)
})
}
3 changes: 2 additions & 1 deletion gno.land/pkg/gnoweb/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ dev:
# Go server in development mode
dev.gnoweb: generate
$(run_reflex) -s -r '.*\.(go|html)' -- \
go run ../../cmd/gnoweb -assets-dir=${PUBLIC_DIR} -chainid=${CHAIN_ID} -remote=${DEV_REMOTE} \
go run ../../cmd/gnoweb -no-strict -assets-dir=${PUBLIC_DIR} -chainid=${CHAIN_ID} -remote=${DEV_REMOTE} \

2>&1 | $(run_logname) gnoweb

# Tailwind CSS in development mode
Expand Down
2 changes: 1 addition & 1 deletion gno.land/pkg/gnoweb/components/ui/icons.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{{ define "ui/icons" }}
<svg xmlns="http://www.w3.org/2000/svg" style="display: none">
<svg xmlns="http://www.w3.org/2000/svg" class="hidden">
<symbol id="ico-search" viewBox="0 0 14 14">
<title>Search</title>
<path
Expand Down

0 comments on commit 9884ba1

Please sign in to comment.