Files
remark42/backend/vendor/github.com/go-pkgz/rest/secure.go
T
dependabot[bot]GitHubpaskaldependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
638fa63e81 Bump the go-modules-updates group in /backend with 7 updates (#1995)
* Bump the go-modules-updates group in /backend with 7 updates

Bumps the go-modules-updates group in /backend with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) | `2.21.1` | `2.23.1` |
| [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) | `5.2.3` | `5.2.4` |
| [github.com/go-pkgz/rest](https://github.com/go-pkgz/rest) | `1.20.6` | `1.21.0` |
| [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) | `5.3.0` | `5.3.1` |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.46.0` | `0.47.0` |
| [golang.org/x/image](https://github.com/golang/image) | `0.34.0` | `0.35.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.48.0` | `0.49.0` |


Updates `github.com/alecthomas/chroma/v2` from 2.21.1 to 2.23.1
- [Release notes](https://github.com/alecthomas/chroma/releases)
- [Commits](https://github.com/alecthomas/chroma/compare/v2.21.1...v2.23.1)

Updates `github.com/go-chi/chi/v5` from 5.2.3 to 5.2.4
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.2.3...v5.2.4)

Updates `github.com/go-pkgz/rest` from 1.20.6 to 1.21.0
- [Release notes](https://github.com/go-pkgz/rest/releases)
- [Commits](https://github.com/go-pkgz/rest/compare/v1.20.6...v1.21.0)

Updates `github.com/golang-jwt/jwt/v5` from 5.3.0 to 5.3.1
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Commits](https://github.com/golang-jwt/jwt/compare/v5.3.0...v5.3.1)

Updates `golang.org/x/crypto` from 0.46.0 to 0.47.0
- [Commits](https://github.com/golang/crypto/compare/v0.46.0...v0.47.0)

Updates `golang.org/x/image` from 0.34.0 to 0.35.0
- [Commits](https://github.com/golang/image/compare/v0.34.0...v0.35.0)

Updates `golang.org/x/net` from 0.48.0 to 0.49.0
- [Commits](https://github.com/golang/net/compare/v0.48.0...v0.49.0)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/chroma/v2
  dependency-version: 2.23.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: github.com/go-chi/chi/v5
  dependency-version: 5.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules-updates
- dependency-name: github.com/go-pkgz/rest
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: github.com/golang-jwt/jwt/v5
  dependency-version: 5.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/crypto
  dependency-version: 0.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/image
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
- dependency-name: golang.org/x/net
  dependency-version: 0.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules-updates
...

Signed-off-by: dependabot[bot] <support@github.com>

* Run go mod tidy in examples directory

Co-authored-by: paskal <712534+paskal@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: paskal <712534+paskal@users.noreply.github.com>
2026-02-14 19:48:24 -06:00

208 lines
6.7 KiB
Go

package rest
import (
"net/http"
"strconv"
"strings"
)
// SecureConfig defines security headers configuration.
// Use SecOpt functions to customize.
type SecureConfig struct {
// xFrameOptions sets X-Frame-Options header. Default: DENY
XFrameOptions string
// xContentTypeOptions sets X-Content-Type-Options. Default: nosniff
XContentTypeOptions string
// ReferrerPolicy sets Referrer-Policy header. Default: strict-origin-when-cross-origin
ReferrerPolicy string
// ContentSecurityPolicy sets Content-Security-Policy header. Default: empty (not set)
ContentSecurityPolicy string
// PermissionsPolicy sets Permissions-Policy header. Default: empty (not set)
PermissionsPolicy string
// sTSSeconds sets max-age for Strict-Transport-Security. 0 disables.
// only sent when request uses HTTPS. Default: 31536000 (1 year)
STSSeconds int
// sTSIncludeSubdomains adds includeSubDomains to HSTS. Default: true
STSIncludeSubdomains bool
// sTSPreload adds preload flag to HSTS. Default: false
STSPreload bool
// xSSProtection sets X-XSS-Protection header. Default: 1; mode=block
// note: this header is deprecated in modern browsers but still useful for older ones
XSSProtection string
}
// SecOpt is a functional option for SecureConfig
type SecOpt func(*SecureConfig)
// defaultSecureConfig returns config with sensible defaults
func defaultSecureConfig() SecureConfig {
return SecureConfig{
XFrameOptions: "DENY",
XContentTypeOptions: "nosniff",
ReferrerPolicy: "strict-origin-when-cross-origin",
STSSeconds: 31536000, // 1 year
STSIncludeSubdomains: true,
STSPreload: false,
XSSProtection: "1; mode=block",
}
}
// SecFrameOptions sets X-Frame-Options header.
// Common values: "DENY", "SAMEORIGIN"
func SecFrameOptions(value string) SecOpt {
return func(c *SecureConfig) {
c.XFrameOptions = value
}
}
// SecContentTypeNosniff enables or disables X-Content-Type-Options: nosniff
func SecContentTypeNosniff(enable bool) SecOpt {
return func(c *SecureConfig) {
if enable {
c.XContentTypeOptions = "nosniff"
} else {
c.XContentTypeOptions = ""
}
}
}
// SecReferrerPolicy sets Referrer-Policy header.
// Common values: "no-referrer", "same-origin", "strict-origin", "strict-origin-when-cross-origin"
func SecReferrerPolicy(policy string) SecOpt {
return func(c *SecureConfig) {
c.ReferrerPolicy = policy
}
}
// SecContentSecurityPolicy sets Content-Security-Policy header.
// Example: "default-src 'self'; script-src 'self'"
func SecContentSecurityPolicy(policy string) SecOpt {
return func(c *SecureConfig) {
c.ContentSecurityPolicy = policy
}
}
// SecPermissionsPolicy sets Permissions-Policy header.
// Example: "geolocation=(), microphone=()"
func SecPermissionsPolicy(policy string) SecOpt {
return func(c *SecureConfig) {
c.PermissionsPolicy = policy
}
}
// SecHSTS configures Strict-Transport-Security header.
// maxAge is in seconds (0 disables HSTS), includeSubdomains and preload are optional flags.
// Note: HSTS header is only sent when the request is over HTTPS.
func SecHSTS(maxAge int, includeSubdomains, preload bool) SecOpt {
return func(c *SecureConfig) {
c.STSSeconds = maxAge
c.STSIncludeSubdomains = includeSubdomains
c.STSPreload = preload
}
}
// SecXSSProtection sets X-XSS-Protection header.
// Set to empty string to disable. Common values: "0", "1", "1; mode=block"
func SecXSSProtection(value string) SecOpt {
return func(c *SecureConfig) {
c.XSSProtection = value
}
}
// SecAllHeaders is a convenience option to set common headers for secure web applications.
// Sets CSP with self-only policy and restrictive permissions.
func SecAllHeaders() SecOpt {
return func(c *SecureConfig) {
c.ContentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; form-action 'self'; frame-ancestors 'none'"
c.PermissionsPolicy = "geolocation=(), microphone=(), camera=()"
}
}
// Secure is middleware that adds security headers to responses.
// By default it sets: X-Frame-Options, X-Content-Type-Options, Referrer-Policy,
// X-XSS-Protection, and Strict-Transport-Security (for HTTPS only).
// Use SecOpt functions to customize the configuration.
func Secure(opts ...SecOpt) func(http.Handler) http.Handler {
cfg := defaultSecureConfig()
for _, opt := range opts {
opt(&cfg)
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// set security headers
if cfg.XFrameOptions != "" {
w.Header().Set("X-Frame-Options", cfg.XFrameOptions)
}
if cfg.XContentTypeOptions != "" {
w.Header().Set("X-Content-Type-Options", cfg.XContentTypeOptions)
}
if cfg.ReferrerPolicy != "" {
w.Header().Set("Referrer-Policy", cfg.ReferrerPolicy)
}
if cfg.XSSProtection != "" {
w.Header().Set("X-XSS-Protection", cfg.XSSProtection)
}
if cfg.ContentSecurityPolicy != "" {
w.Header().Set("Content-Security-Policy", cfg.ContentSecurityPolicy)
}
if cfg.PermissionsPolicy != "" {
w.Header().Set("Permissions-Policy", cfg.PermissionsPolicy)
}
// HSTS only for HTTPS connections
if cfg.STSSeconds > 0 && isHTTPS(r) {
sts := "max-age=" + strconv.Itoa(cfg.STSSeconds)
if cfg.STSIncludeSubdomains {
sts += "; includeSubDomains"
}
if cfg.STSPreload {
sts += "; preload"
}
w.Header().Set("Strict-Transport-Security", sts)
}
next.ServeHTTP(w, r)
})
}
}
// isHTTPS checks if the request is over HTTPS by examining TLS state and common proxy headers
func isHTTPS(r *http.Request) bool {
// direct TLS connection
if r.TLS != nil {
return true
}
// check common proxy headers (case-insensitive)
if strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") {
return true
}
// check RFC 7239 Forwarded header
if forwarded := r.Header.Get("Forwarded"); forwarded != "" {
if forwardedProtoIsHTTPS(forwarded) {
return true
}
}
return false
}
// forwardedProtoIsHTTPS parses RFC 7239 Forwarded header to check for proto=https.
// The header format is: Forwarded: for=1.2.3.4;proto=https;by=proxy, for=5.6.7.8
// Parameters are separated by semicolons, multiple forwarded elements by commas.
func forwardedProtoIsHTTPS(header string) bool {
// split by comma to get individual forwarded elements
for element := range strings.SplitSeq(header, ",") {
// split by semicolon to get parameters within element
for param := range strings.SplitSeq(element, ";") {
param = strings.TrimSpace(param)
// check for proto=https (case-insensitive per RFC 7239)
if len(param) > 6 && strings.EqualFold(param[:6], "proto=") {
if strings.EqualFold(strings.TrimSpace(param[6:]), "https") {
return true
}
}
}
}
return false
}