mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-03 08:02:04 +00:00
68 lines
2.6 KiB
Go
68 lines
2.6 KiB
Go
package middleware
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// SecurityHeaders sets the response headers that tell a browser what it may do
|
|
// with an API response. None of them were being sent by any layer: not the Go
|
|
// services, not the nginx images, not the edge.
|
|
//
|
|
// The API answers JSON, so the policy can be the strictest one there is. It
|
|
// loads nothing, frames nothing, and may not be framed. That matters because
|
|
// the same origin also serves /public (customer uploads) and the OAuth bouncer
|
|
// pages, and those are the responses an attacker would want rendered.
|
|
//
|
|
// HSTS is only emitted on a request that actually arrived over TLS. Sending it
|
|
// over plain HTTP is ignored by browsers anyway, and a self-hosted instance
|
|
// deliberately running on a LAN without TLS must not be pinned to a scheme it
|
|
// does not serve.
|
|
func SecurityHeaders() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
h := c.Writer.Header()
|
|
|
|
// A browser must never guess a content type here: /public serves
|
|
// customer uploads from this origin.
|
|
h.Set("X-Content-Type-Options", "nosniff")
|
|
h.Set("X-Frame-Options", "DENY")
|
|
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
|
h.Set("Cross-Origin-Opener-Policy", "same-origin")
|
|
h.Set("Cross-Origin-Resource-Policy", "same-site")
|
|
h.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), interest-cohort=()")
|
|
|
|
// default-src 'none' suits a JSON API. The handful of routes that
|
|
// return HTML (the OAuth bouncers, the unsubscribe page) set their own
|
|
// policy over this one, and /public sets an image sandbox.
|
|
if _, already := h["Content-Security-Policy"]; !already {
|
|
h.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'")
|
|
}
|
|
|
|
if requestIsHTTPS(c) {
|
|
h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// requestIsHTTPS reports whether the client reached us over TLS. Termination is
|
|
// always upstream (Railway, Caddy, nginx), so the forwarded header is the
|
|
// signal.
|
|
//
|
|
// The header is read directly: gin's trusted-proxy handling covers ClientIP,
|
|
// not arbitrary headers. That is acceptable here because the only thing it
|
|
// decides is whether to send HSTS, a browser cannot set the header on a request
|
|
// it makes, and a non-browser client that sets it receives a header it ignores.
|
|
// Nothing is authorized on this value.
|
|
func requestIsHTTPS(c *gin.Context) bool {
|
|
if c.Request.TLS != nil {
|
|
return true
|
|
}
|
|
if proto := c.GetHeader("X-Forwarded-Proto"); proto != "" {
|
|
return strings.EqualFold(strings.TrimSpace(strings.Split(proto, ",")[0]), "https")
|
|
}
|
|
return false
|
|
}
|