diff --git a/gno.land/cmd/gnoweb/main.go b/gno.land/cmd/gnoweb/main.go index 8c0df00aa35..a862147308c 100644 --- a/gno.land/cmd/gnoweb/main.go +++ b/gno.land/cmd/gnoweb/main.go @@ -23,9 +23,11 @@ type webCfg struct { bind string faucetURL string assetsDir string + timeout time.Duration analytics bool json bool html bool + noStrict bool verbose bool } @@ -33,6 +35,7 @@ var defaultWebOptions = webCfg{ chainid: "dev", remote: "127.0.0.1:26657", bind: ":8888", + timeout: time.Minute, } func main() { @@ -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( @@ -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) { @@ -179,11 +196,17 @@ 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 { @@ -191,6 +214,41 @@ func setupWeb(cfg *webCfg, _ []string, io commands.IO) (func() error, error) { 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; includeSubDomains; preload") + } + + next.ServeHTTP(w, r) + }) +} diff --git a/gno.land/pkg/gnoweb/Makefile b/gno.land/pkg/gnoweb/Makefile index c8d662ec3b5..be7c2c2a3a2 100644 --- a/gno.land/pkg/gnoweb/Makefile +++ b/gno.land/pkg/gnoweb/Makefile @@ -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