Skip to content

Conversation

@umputun
Copy link
Member

@umputun umputun commented Jan 8, 2026

New middlewares for common security and URL normalization needs:

  • Secure - adds security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, HSTS, CSP, etc.)
  • CORS - cross-origin resource sharing with configurable origins, methods, headers, and credentials support
  • CSRF - cross-site request forgery protection using Fetch metadata headers (wraps stdlib on Go 1.25+)
  • CleanPath - removes double slashes from URL paths while preserving encoding
  • StripSlashes - removes trailing slashes from URL paths

All middlewares follow functional options pattern for configuration. Full test coverage (96.9%) and documentation included.

configurable middleware adding security headers:
- X-Frame-Options, X-Content-Type-Options, Referrer-Policy
- X-XSS-Protection, Strict-Transport-Security (HTTPS only)
- optional Content-Security-Policy and Permissions-Policy
cross-origin resource sharing middleware with:
- configurable allowed origins, methods, headers
- automatic preflight handling
- credentials support with origin reflection
- preflight result caching
cross-site request forgery protection using Fetch metadata:
- checks Sec-Fetch-Site and Origin headers
- trusted origin allowlist support
- bypass patterns for webhooks/OAuth
- wraps stdlib http.CrossOriginProtection on Go 1.25+
CleanPath: removes double slashes from URL path
- /users//1 becomes /users/1
- preserves trailing slashes and dot segments
- preserves percent-encoding in RawPath

StripSlashes: removes trailing slashes
- /users/ becomes /users
- preserves root path /
document Secure, CORS, CSRF, CleanPath, and StripSlashes
middlewares with usage examples and configuration options
Copilot AI review requested due to automatic review settings January 8, 2026 09:02
@umputun umputun merged commit 4e8fe40 into master Jan 8, 2026
6 checks passed
@umputun umputun deleted the new-middlewares branch January 8, 2026 09:03
@coveralls
Copy link

Pull Request Test Coverage Report for Build 20811297094

Details

  • 374 of 385 (97.14%) changed or added relevant lines in 4 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.5%) to 95.99%

Changes Missing Coverage Covered Lines Changed/Added Lines %
secure.go 109 110 99.09%
csrf.go 117 127 92.13%
Totals Coverage Status
Change from base Build 20809181256: 0.5%
Covered Lines: 1173
Relevant Lines: 1222

💛 - Coveralls

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds five new security and URL normalization middlewares to enhance the REST package's security capabilities and request handling:

  • Secure: Adds security headers (X-Frame-Options, CSP, HSTS, etc.) with configurable options
  • CORS: Cross-Origin Resource Sharing with support for credentials and preflight handling
  • CSRF: Cross-Site Request Forgery protection using Fetch metadata headers, with dual implementation (Go 1.25+ stdlib wrapper and custom for earlier versions)
  • CleanPath: Removes double slashes from URL paths while preserving encoding
  • StripSlashes: Removes trailing slashes from URL paths

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
secure.go Implements Secure middleware with security headers and HTTPS detection via TLS/proxy headers
secure_test.go Comprehensive tests for Secure middleware covering defaults, custom options, and HTTPS detection
cors.go Implements CORS middleware with origin validation, preflight handling, and credentials support
cors_test.go Tests for CORS including preflight, origin matching, credentials, and integration scenarios
csrf.go Custom CSRF implementation for Go <1.25 using Sec-Fetch-Site and Origin header validation
csrf_go125.go Wrapper for stdlib http.CrossOriginProtection in Go 1.25+
csrf_test.go Tests for CSRF protection covering safe methods, origin validation, bypass patterns, and trusted origins
rewrite.go Adds CleanPath and StripSlashes middlewares for URL path normalization
rewrite_test.go Tests for CleanPath and StripSlashes including edge cases and RawPath handling
README.md Documentation for all five new middlewares with usage examples and configuration options

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// when true, AllowedOrigins cannot be "*" (browser security restriction).
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment starts with lowercase "when" but should start with an uppercase letter to follow Go documentation conventions.

Suggested change
// when true, AllowedOrigins cannot be "*" (browser security restriction).
// When true, AllowedOrigins cannot be "*" (browser security restriction).

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +37
// default: ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use.
// default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
AllowedMethods []string
// AllowedHeaders is a list of headers the client is allowed to send.
// default: Accept, Content-Type, Authorization, X-Requested-With
AllowedHeaders []string
// ExposedHeaders is a list of headers that are safe to expose to the client.
// default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// when true, AllowedOrigins cannot be "*" (browser security restriction).
// default: false
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
// default: 0 (no caching)
MaxAge int
}

// CorsOpt is a functional option for CORSConfig
type CorsOpt func(*CORSConfig)

// defaultCORSConfig returns config with sensible defaults
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment starts with lowercase "default" but should start with an uppercase letter to follow Go documentation conventions.

Suggested change
// default: ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use.
// default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
AllowedMethods []string
// AllowedHeaders is a list of headers the client is allowed to send.
// default: Accept, Content-Type, Authorization, X-Requested-With
AllowedHeaders []string
// ExposedHeaders is a list of headers that are safe to expose to the client.
// default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// when true, AllowedOrigins cannot be "*" (browser security restriction).
// default: false
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
// default: 0 (no caching)
MaxAge int
}
// CorsOpt is a functional option for CORSConfig
type CorsOpt func(*CORSConfig)
// defaultCORSConfig returns config with sensible defaults
// Default: ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use.
// Default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
AllowedMethods []string
// AllowedHeaders is a list of headers the client is allowed to send.
// Default: Accept, Content-Type, Authorization, X-Requested-With
AllowedHeaders []string
// ExposedHeaders is a list of headers that are safe to expose to the client.
// Default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// when true, AllowedOrigins cannot be "*" (browser security restriction).
// Default: false
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
// Default: 0 (no caching)
MaxAge int
}
// CorsOpt is a functional option for CORSConfig
type CorsOpt func(*CORSConfig)
// DefaultCORSConfig returns config with sensible defaults.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +29
// 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
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment has lowercase field name reference instead of the actual field name. The comment says "sTSSeconds" but the actual field name is "STSSeconds" (capitalized). This same issue appears for "sTSIncludeSubdomains" (line 25) and "sTSPreload" (line 27).

Suggested change
// 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
// 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

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +30
// default: ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use.
// default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
AllowedMethods []string
// AllowedHeaders is a list of headers the client is allowed to send.
// default: Accept, Content-Type, Authorization, X-Requested-With
AllowedHeaders []string
// ExposedHeaders is a list of headers that are safe to expose to the client.
// default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// when true, AllowedOrigins cannot be "*" (browser security restriction).
// default: false
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
// default: 0 (no caching)
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment starts with lowercase "default" but should start with an uppercase letter to follow Go documentation conventions.

Suggested change
// default: ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use.
// default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
AllowedMethods []string
// AllowedHeaders is a list of headers the client is allowed to send.
// default: Accept, Content-Type, Authorization, X-Requested-With
AllowedHeaders []string
// ExposedHeaders is a list of headers that are safe to expose to the client.
// default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// when true, AllowedOrigins cannot be "*" (browser security restriction).
// default: false
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
// default: 0 (no caching)
// Default: ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use.
// Default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
AllowedMethods []string
// AllowedHeaders is a list of headers the client is allowed to send.
// Default: Accept, Content-Type, Authorization, X-Requested-With
AllowedHeaders []string
// ExposedHeaders is a list of headers that are safe to expose to the client.
// Default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// when true, AllowedOrigins cannot be "*" (browser security restriction).
// Default: false
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
// Default: 0 (no caching)

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +30
// default: ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use.
// default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
AllowedMethods []string
// AllowedHeaders is a list of headers the client is allowed to send.
// default: Accept, Content-Type, Authorization, X-Requested-With
AllowedHeaders []string
// ExposedHeaders is a list of headers that are safe to expose to the client.
// default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// when true, AllowedOrigins cannot be "*" (browser security restriction).
// default: false
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
// default: 0 (no caching)
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment starts with lowercase "default" but should start with an uppercase letter to follow Go documentation conventions.

Suggested change
// default: ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use.
// default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
AllowedMethods []string
// AllowedHeaders is a list of headers the client is allowed to send.
// default: Accept, Content-Type, Authorization, X-Requested-With
AllowedHeaders []string
// ExposedHeaders is a list of headers that are safe to expose to the client.
// default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// when true, AllowedOrigins cannot be "*" (browser security restriction).
// default: false
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
// default: 0 (no caching)
// Default: ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use.
// Default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
AllowedMethods []string
// AllowedHeaders is a list of headers the client is allowed to send.
// Default: Accept, Content-Type, Authorization, X-Requested-With
AllowedHeaders []string
// ExposedHeaders is a list of headers that are safe to expose to the client.
// Default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// when true, AllowedOrigins cannot be "*" (browser security restriction).
// Default: false
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
// Default: 0 (no caching)

Copilot uses AI. Check for mistakes.
// SecureConfig defines security headers configuration.
// Use SecOpt functions to customize.
type SecureConfig struct {
// xFrameOptions sets X-Frame-Options header. Default: DENY
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment has lowercase field name references instead of the actual field names. The comment says "xFrameOptions" but the actual field name is "XFrameOptions" (capitalized). This inconsistency appears in multiple comment lines.

Copilot uses AI. Check for mistakes.
type SecureConfig struct {
// xFrameOptions sets X-Frame-Options header. Default: DENY
XFrameOptions string
// xContentTypeOptions sets X-Content-Type-Options. Default: nosniff
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment has lowercase field name references instead of the actual field names. The comment says "xContentTypeOptions" but the actual field name is "XContentTypeOptions" (capitalized).

Copilot uses AI. Check for mistakes.
// Use CorsOpt functions to customize.
type CORSConfig struct {
// AllowedOrigins is a list of origins that may access the resource.
// use "*" to allow all origins (not recommended with credentials).
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment starts with lowercase "use" but should start with an uppercase letter to follow Go documentation conventions.

Suggested change
// use "*" to allow all origins (not recommended with credentials).
// Use "*" to allow all origins (not recommended with credentials).

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +30
// use "*" to allow all origins (not recommended with credentials).
// default: ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use.
// default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
AllowedMethods []string
// AllowedHeaders is a list of headers the client is allowed to send.
// default: Accept, Content-Type, Authorization, X-Requested-With
AllowedHeaders []string
// ExposedHeaders is a list of headers that are safe to expose to the client.
// default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// when true, AllowedOrigins cannot be "*" (browser security restriction).
// default: false
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
// default: 0 (no caching)
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment starts with lowercase "default" but should start with an uppercase letter to follow Go documentation conventions.

Suggested change
// use "*" to allow all origins (not recommended with credentials).
// default: ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use.
// default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
AllowedMethods []string
// AllowedHeaders is a list of headers the client is allowed to send.
// default: Accept, Content-Type, Authorization, X-Requested-With
AllowedHeaders []string
// ExposedHeaders is a list of headers that are safe to expose to the client.
// default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// when true, AllowedOrigins cannot be "*" (browser security restriction).
// default: false
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
// default: 0 (no caching)
// Use "*" to allow all origins (not recommended with credentials).
// Default: ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use.
// Default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
AllowedMethods []string
// AllowedHeaders is a list of headers the client is allowed to send.
// Default: Accept, Content-Type, Authorization, X-Requested-With
AllowedHeaders []string
// ExposedHeaders is a list of headers that are safe to expose to the client.
// Default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// When true, AllowedOrigins cannot be "*" (browser security restriction).
// Default: false
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
// Default: 0 (no caching)

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +30
// use "*" to allow all origins (not recommended with credentials).
// default: ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use.
// default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
AllowedMethods []string
// AllowedHeaders is a list of headers the client is allowed to send.
// default: Accept, Content-Type, Authorization, X-Requested-With
AllowedHeaders []string
// ExposedHeaders is a list of headers that are safe to expose to the client.
// default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// when true, AllowedOrigins cannot be "*" (browser security restriction).
// default: false
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
// default: 0 (no caching)
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment starts with lowercase "default" but should start with an uppercase letter to follow Go documentation conventions.

Suggested change
// use "*" to allow all origins (not recommended with credentials).
// default: ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use.
// default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
AllowedMethods []string
// AllowedHeaders is a list of headers the client is allowed to send.
// default: Accept, Content-Type, Authorization, X-Requested-With
AllowedHeaders []string
// ExposedHeaders is a list of headers that are safe to expose to the client.
// default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// when true, AllowedOrigins cannot be "*" (browser security restriction).
// default: false
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
// default: 0 (no caching)
// Use "*" to allow all origins (not recommended with credentials).
// Default: ["*"]
AllowedOrigins []string
// AllowedMethods is a list of methods the client is allowed to use.
// Default: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
AllowedMethods []string
// AllowedHeaders is a list of headers the client is allowed to send.
// Default: Accept, Content-Type, Authorization, X-Requested-With
AllowedHeaders []string
// ExposedHeaders is a list of headers that are safe to expose to the client.
// Default: empty
ExposedHeaders []string
// AllowCredentials indicates whether the request can include credentials.
// When true, AllowedOrigins cannot be "*" (browser security restriction).
// Default: false
AllowCredentials bool
// MaxAge indicates how long (in seconds) the results of a preflight can be cached.
// Default: 0 (no caching)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants