spire-http-0.1.0.0: src/Spire/Http/SecureHeaders.hs
-- | OWASP-recommended security headers middleware.
--
-- Adds security headers to every response. Configurable via record
-- update on 'SecureHeadersConfig'.
--
-- Default headers:
--
-- * @X-Content-Type-Options: nosniff@
-- * @X-Frame-Options: DENY@
-- * @X-XSS-Protection: 0@ (modern browsers don't need it; can cause issues)
-- * @Referrer-Policy: strict-origin-when-cross-origin@
-- * @Content-Security-Policy: default-src 'self'@
-- * @Permissions-Policy: camera=(), microphone=(), geolocation=()@
--
-- Optional: @Strict-Transport-Security@ (HSTS) — off by default since
-- it requires HTTPS.
module Spire.Http.SecureHeaders
( -- * Config
SecureHeadersConfig (..)
, defaultSecureHeaders
-- * Layer
, secureHeadersLayer
) where
import Data.ByteString (ByteString)
import Network.HTTP.Types (ResponseHeaders, Header)
import Spire (Middleware, middleware)
import Spire.Service (Service (..), mapResponse)
import Http.Core (Request, Response (..))
-- | Configuration for security headers.
data SecureHeadersConfig = SecureHeadersConfig
{ xContentTypeOptions :: !ByteString
, xFrameOptions :: !ByteString
, xXssProtection :: !ByteString
, referrerPolicy :: !ByteString
, contentSecurityPolicy :: !ByteString
, permissionsPolicy :: !ByteString
, strictTransportSecurity :: !(Maybe ByteString)
-- ^ @Nothing@ = don't add HSTS. @Just "max-age=63072000"@ for 2 years.
}
-- | Sensible defaults matching OWASP recommendations.
defaultSecureHeaders :: SecureHeadersConfig
defaultSecureHeaders = SecureHeadersConfig
{ xContentTypeOptions = "nosniff"
, xFrameOptions = "DENY"
, xXssProtection = "0"
, referrerPolicy = "strict-origin-when-cross-origin"
, contentSecurityPolicy = "default-src 'self'"
, permissionsPolicy = "camera=(), microphone=(), geolocation=()"
, strictTransportSecurity = Nothing
}
-- | Build the header list from config.
configHeaders :: SecureHeadersConfig -> ResponseHeaders
configHeaders cfg = base ++ hsts
where
base =
[ ("X-Content-Type-Options", xContentTypeOptions cfg)
, ("X-Frame-Options", xFrameOptions cfg)
, ("X-XSS-Protection", xXssProtection cfg)
, ("Referrer-Policy", referrerPolicy cfg)
, ("Content-Security-Policy", contentSecurityPolicy cfg)
, ("Permissions-Policy", permissionsPolicy cfg)
]
hsts = case strictTransportSecurity cfg of
Nothing -> []
Just val -> [("Strict-Transport-Security", val)]
-- | Middleware that adds security headers to every response.
secureHeadersLayer
:: SecureHeadersConfig
-> Middleware IO (Request ByteString) (Response ByteString)
secureHeadersLayer cfg = middleware $ \inner ->
mapResponse addHeaders inner
where
hdrs = configHeaders cfg
addHeaders resp = resp
{ responseHeaders = responseHeaders resp ++ hdrs }