Secure
Secure is an HTTP middleware for Go that facilitates some quick security wins. It's a standard net/http Handler, and can be used with many frameworks or directly with Go's net/http package.
Usage
package main
import (
"net/http"
"github.com/unrolled/secure"
)
var myHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world"))
})
func main() {
secureMiddleware := secure.New(secure.Options{
AllowedHosts: []string{"example\\.com", ".*\\.example\\.com"},
AllowedHostsAreRegex: true,
HostsProxyHeaders: []string{"X-Forwarded-Host"},
SSLRedirect: true,
SSLHost: "ssl.example.com",
SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"},
STSSeconds: 31536000,
STSIncludeSubdomains: true,
STSPreload: true,
FrameDeny: true,
ContentTypeNosniff: true,
BrowserXssFilter: true,
ContentSecurityPolicy: "script-src $NONCE",
})
app := secureMiddleware.Handler(myHandler)
http.ListenAndServe("127.0.0.1:3000", app)
}
Be sure to include the Secure middleware as close to the top (beginning) as possible (but after logging and recovery). It's best to do the allowed hosts and SSL check before anything else.
The above example will only allow requests with a host name of 'example.com', or 'ssl.example.com'. Also if the request is not HTTPS, it will be redirected to HTTPS with the host name of 'ssl.example.com'.
Once those requirements are satisfied, it will add the following headers:
Strict-Transport-Security: 31536000; includeSubdomains; preload
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Security-Policy: script-src 'nonce-a2ZobGFoZg=='
Set the IsDevelopment
option to true
when developing!
When IsDevelopment
is true, the AllowedHosts, SSLRedirect, and STS header will not be in effect. This allows you to work in development/test mode and not have any annoying redirects to HTTPS (ie. development can happen on HTTP), or block localhost
has a bad host.
Available options
Secure comes with a variety of configuration options (Note: these are not the default option values. See the defaults below.):
s := secure.New(secure.Options{
AllowedHosts: []string{"ssl.example.com"},
AllowedHostsAreRegex: false,
AllowRequestFunc: nil,
HostsProxyHeaders: []string{"X-Forwarded-Hosts"},
SSLRedirect: true,
SSLTemporaryRedirect: false,
SSLHost: "ssl.example.com",
SSLHostFunc: nil,
SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"},
STSSeconds: 31536000,
STSIncludeSubdomains: true,
STSPreload: true,
ForceSTSHeader: false,
FrameDeny: true,
CustomFrameOptionsValue: "SAMEORIGIN",
ContentTypeNosniff: true,
BrowserXssFilter: true,
CustomBrowserXssValue: "1; report=https://example.com/xss-report",
ContentSecurityPolicy: "default-src 'self'",
ReferrerPolicy: "same-origin",
FeaturePolicy: "vibrate 'none';",
PermissionsPolicy: "fullscreen=(), geolocation=()",
CrossOriginOpenerPolicy: "same-origin",
CrossOriginEmbedderPolicy: "require-corp",
CrossOriginResourcePolicy: "same-origin",
XDNSPrefetchControl: "on",
XPermittedCrossDomainPolicies: "none",
IsDevelopment: true,
})
Default options
These are the preset options for Secure:
s := secure.New()
l := secure.New(secure.Options{
AllowedHosts: []string,
AllowedHostsAreRegex: false,
AllowRequestFunc: nil,
HostsProxyHeaders: []string,
SSLRedirect: false,
SSLTemporaryRedirect: false,
SSLHost: "",
SSLProxyHeaders: map[string]string{},
STSSeconds: 0,
STSIncludeSubdomains: false,
STSPreload: false,
ForceSTSHeader: false,
FrameDeny: false,
CustomFrameOptionsValue: "",
ContentTypeNosniff: false,
BrowserXssFilter: false,
ContentSecurityPolicy: "",
PublicKey: "",
ReferrerPolicy: "",
FeaturePolicy: "",
PermissionsPolicy: "",
CrossOriginOpenerPolicy: "",
CrossOriginEmbedderPolicy: "",
CrossOriginResourcePolicy: "",
XDNSPrefetchControl: "",
XPermittedCrossDomainPolicies: "",
IsDevelopment: false,
})
The default bad host handler returns the following error:
http.Error(w, "Bad Host", http.StatusInternalServerError)
Call secure.SetBadHostHandler
to set your own custom handler.
The default bad request handler returns the following error:
http.Error(w, "Bad Request", http.StatusBadRequest)
Call secure.SetBadRequestHandler
to set your own custom handler.
Allow Request Function
Secure allows you to set a custom function (func(r *http.Request) bool
) for the AllowRequestFunc
option. You can use this function as a custom filter to allow the request to continue or simply reject it. This can be handy if you need to do any dynamic filtering on any of the request properties. It should be noted that this function will be called on every request, so be sure to make your checks quick and not relying on time consuming external calls (or you will be slowing down all requests). See above on how to set a custom handler for the rejected requests.
Redirecting HTTP to HTTPS
If you want to redirect all HTTP requests to HTTPS, you can use the following example.
package main
import (
"log"
"net/http"
"github.com/unrolled/secure"
)
var myHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world"))
})
func main() {
secureMiddleware := secure.New(secure.Options{
SSLRedirect: true,
SSLHost: "localhost:8443",
})
app := secureMiddleware.Handler(myHandler)
go func() {
log.Fatal(http.ListenAndServe(":8080", app))
}()
log.Fatal(http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", app))
}
Strict Transport Security
The STS header will only be sent on verified HTTPS connections (and when IsDevelopment
is false). Be sure to set the SSLProxyHeaders
option if your application is behind a proxy to ensure the proper behavior. If you need the STS header for all HTTP and HTTPS requests (which you shouldn't), you can use the ForceSTSHeader
option. Note that if IsDevelopment
is true, it will still disable this header even when ForceSTSHeader
is set to true.
- The
preload
flag is required for domain inclusion in Chrome's preload list.
Content Security Policy
You can utilize the CSP Builder to create your policies:
import (
"github.com/unrolled/secure"
"github.com/unrolled/secure/cspbuilder"
)
cspBuilder := cspbuilder.Builder{
Directives: map[string][]string{
cspbuilder.DefaultSrc: {"self"},
cspbuilder.ScriptSrc: {"self", "www.google-analytics.com"},
cspbuilder.ImgSrc: {"*"},
},
}
opt := secure.Options{
ContentSecurityPolicy: cspBuilder.MustBuild(),
}
Integration examples
package main
import (
"net/http"
"github.com/pressly/chi"
"github.com/unrolled/secure"
)
func main() {
secureMiddleware := secure.New(secure.Options{
FrameDeny: true,
})
r := chi.NewRouter()
r.Use(secureMiddleware.Handler)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("X-Frame-Options header is now `DENY`."))
})
http.ListenAndServe("127.0.0.1:3000", r)
}
package main
import (
"net/http"
"github.com/labstack/echo"
"github.com/unrolled/secure"
)
func main() {
secureMiddleware := secure.New(secure.Options{
FrameDeny: true,
})
e := echo.New()
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "X-Frame-Options header is now `DENY`.")
})
e.Use(echo.WrapMiddleware(secureMiddleware.Handler))
e.Logger.Fatal(e.Start("127.0.0.1:3000"))
}
package main
import (
"github.com/gin-gonic/gin"
"github.com/unrolled/secure"
)
func main() {
secureMiddleware := secure.New(secure.Options{
FrameDeny: true,
})
secureFunc := func() gin.HandlerFunc {
return func(c *gin.Context) {
err := secureMiddleware.Process(c.Writer, c.Request)
if err != nil {
c.Abort()
return
}
if status := c.Writer.Status(); status > 300 && status < 399 {
c.Abort()
}
}
}()
router := gin.Default()
router.Use(secureFunc)
router.GET("/", func(c *gin.Context) {
c.String(200, "X-Frame-Options header is now `DENY`.")
})
router.Run("127.0.0.1:3000")
}
package main
import (
"net/http"
"github.com/unrolled/secure"
"github.com/zenazn/goji"
"github.com/zenazn/goji/web"
)
func main() {
secureMiddleware := secure.New(secure.Options{
FrameDeny: true,
})
goji.Get("/", func(c web.C, w http.ResponseWriter, req *http.Request) {
w.Write([]byte("X-Frame-Options header is now `DENY`."))
})
goji.Use(secureMiddleware.Handler)
goji.Serve()
}
package main
import (
"github.com/kataras/iris/v12"
"github.com/unrolled/secure"
)
func main() {
app := iris.New()
secureMiddleware := secure.New(secure.Options{
FrameDeny: true,
})
app.Use(iris.FromStd(secureMiddleware.HandlerFuncWithNext))
app.Get("/home", func(ctx iris.Context) {
ctx.Writef("X-Frame-Options header is now `%s`.", "DENY")
})
app.Listen(":8080")
}
package main
import (
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/unrolled/secure"
)
func main() {
secureMiddleware := secure.New(secure.Options{
FrameDeny: true,
})
r := mux.NewRouter()
r.Use(secureMiddleware.Handler)
http.Handle("/", r)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", 8080), nil))
}
Note this implementation has a special helper function called HandlerFuncWithNext
.
package main
import (
"net/http"
"github.com/urfave/negroni"
"github.com/unrolled/secure"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("X-Frame-Options header is now `DENY`."))
})
secureMiddleware := secure.New(secure.Options{
FrameDeny: true,
})
n := negroni.Classic()
n.Use(negroni.HandlerFunc(secureMiddleware.HandlerFuncWithNext))
n.UseHandler(mux)
n.Run("127.0.0.1:3000")
}