Rework access control origin configuration

This commit is contained in:
Traefiker Bot 2020-03-05 08:18:04 +01:00 committed by GitHub
parent fb51ebcba6
commit 082fb166a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 203 additions and 122 deletions

View file

@ -197,7 +197,7 @@ This functionality allows for more advanced security features to quickly be set.
```yaml tab="Docker" ```yaml tab="Docker"
labels: labels:
- "traefik.http.middlewares.testheader.headers.accesscontrolallowmethods=GET,OPTIONS,PUT" - "traefik.http.middlewares.testheader.headers.accesscontrolallowmethods=GET,OPTIONS,PUT"
- "traefik.http.middlewares.testheader.headers.accesscontrolalloworigin=origin-list-or-null" - "traefik.http.middlewares.testheader.headers.accesscontrolalloworiginlist=https://foo.bar.org,https://example.org"
- "traefik.http.middlewares.testheader.headers.accesscontrolmaxage=100" - "traefik.http.middlewares.testheader.headers.accesscontrolmaxage=100"
- "traefik.http.middlewares.testheader.headers.addvaryheader=true" - "traefik.http.middlewares.testheader.headers.addvaryheader=true"
``` ```
@ -213,14 +213,16 @@ spec:
- "GET" - "GET"
- "OPTIONS" - "OPTIONS"
- "PUT" - "PUT"
accessControlAllowOrigin: "origin-list-or-null" accessControlAllowOriginList:
- "https://foo.bar.org"
- "https://example.org"
accessControlMaxAge: 100 accessControlMaxAge: 100
addVaryHeader: "true" addVaryHeader: "true"
``` ```
```yaml tab="Consul Catalog" ```yaml tab="Consul Catalog"
- "traefik.http.middlewares.testheader.headers.accesscontrolallowmethods=GET,OPTIONS,PUT" - "traefik.http.middlewares.testheader.headers.accesscontrolallowmethods=GET,OPTIONS,PUT"
- "traefik.http.middlewares.testheader.headers.accesscontrolalloworigin=origin-list-or-null" - "traefik.http.middlewares.testheader.headers.accesscontrolalloworiginlist=https://foo.bar.org,https://example.org"
- "traefik.http.middlewares.testheader.headers.accesscontrolmaxage=100" - "traefik.http.middlewares.testheader.headers.accesscontrolmaxage=100"
- "traefik.http.middlewares.testheader.headers.addvaryheader=true" - "traefik.http.middlewares.testheader.headers.addvaryheader=true"
``` ```
@ -228,7 +230,7 @@ spec:
```json tab="Marathon" ```json tab="Marathon"
"labels": { "labels": {
"traefik.http.middlewares.testheader.headers.accesscontrolallowmethods": "GET,OPTIONS,PUT", "traefik.http.middlewares.testheader.headers.accesscontrolallowmethods": "GET,OPTIONS,PUT",
"traefik.http.middlewares.testheader.headers.accesscontrolalloworigin": "origin-list-or-null", "traefik.http.middlewares.testheader.headers.accesscontrolalloworiginlist": "https://foo.bar.org,https://example.org",
"traefik.http.middlewares.testheader.headers.accesscontrolmaxage": "100", "traefik.http.middlewares.testheader.headers.accesscontrolmaxage": "100",
"traefik.http.middlewares.testheader.headers.addvaryheader": "true" "traefik.http.middlewares.testheader.headers.addvaryheader": "true"
} }
@ -237,7 +239,7 @@ spec:
```yaml tab="Rancher" ```yaml tab="Rancher"
labels: labels:
- "traefik.http.middlewares.testheader.headers.accesscontrolallowmethods=GET,OPTIONS,PUT" - "traefik.http.middlewares.testheader.headers.accesscontrolallowmethods=GET,OPTIONS,PUT"
- "traefik.http.middlewares.testheader.headers.accesscontrolalloworigin=origin-list-or-null" - "traefik.http.middlewares.testheader.headers.accesscontrolalloworiginlist=https://foo.bar.org,https://example.org"
- "traefik.http.middlewares.testheader.headers.accesscontrolmaxage=100" - "traefik.http.middlewares.testheader.headers.accesscontrolmaxage=100"
- "traefik.http.middlewares.testheader.headers.addvaryheader=true" - "traefik.http.middlewares.testheader.headers.addvaryheader=true"
``` ```
@ -246,7 +248,7 @@ labels:
[http.middlewares] [http.middlewares]
[http.middlewares.testHeader.headers] [http.middlewares.testHeader.headers]
accessControlAllowMethods= ["GET", "OPTIONS", "PUT"] accessControlAllowMethods= ["GET", "OPTIONS", "PUT"]
accessControlAllowOrigin = "origin-list-or-null" accessControlAllowOriginList = ["https://foo.bar.org","https://example.org"]
accessControlMaxAge = 100 accessControlMaxAge = 100
addVaryHeader = true addVaryHeader = true
``` ```
@ -260,7 +262,9 @@ http:
- GET - GET
- OPTIONS - OPTIONS
- PUT - PUT
accessControlAllowOrigin: "origin-list-or-null" accessControlAllowOriginList:
- https://foo.bar.org
- https://example.org
accessControlMaxAge: 100 accessControlMaxAge: 100
addVaryHeader: true addVaryHeader: true
``` ```
@ -295,14 +299,22 @@ The `accessControlAllowHeaders` indicates which header field names can be used a
The `accessControlAllowMethods` indicates which methods can be used during requests. The `accessControlAllowMethods` indicates which methods can be used during requests.
### `accessControlAllowOrigin` ### `accessControlAllowOriginList`
The `accessControlAllowOrigin` indicates whether a resource can be shared by returning different values. The `accessControlAllowOriginList` indicates whether a resource can be shared by returning different values.
The three options for this value are:
- `origin-list-or-null` A wildcard origin `*` can also be configured, and will match all requests.
- `*` If this value is set by a backend server, it will be overwritten by Traefik
- `null`
This value can contains a list of allowed origins.
More information including how to use the settings can be found on:
- [Mozilla.org](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin)
- [w3](https://www.w3.org/TR/cors/#access-control-allow-origin-response-header)
- [IETF](https://tools.ietf.org/html/rfc6454#section-7.1)
Traefik no longer supports the null value, as it is [no longer recommended as a return value](https://w3c.github.io/webappsec-cors-for-developers/#avoid-returning-access-control-allow-origin-null).
### `accessControlExposeHeaders` ### `accessControlExposeHeaders`
@ -314,7 +326,7 @@ The `accessControlMaxAge` indicates how long a preflight request can be cached.
### `addVaryHeader` ### `addVaryHeader`
The `addVaryHeader` is used in conjunction with `accessControlAllowOrigin` to determine whether the vary header should be added or modified to demonstrate that server responses can differ beased on the value of the origin header. The `addVaryHeader` is used in conjunction with `accessControlAllowOriginList` to determine whether the vary header should be added or modified to demonstrate that server responses can differ based on the value of the origin header.
### `allowedHosts` ### `allowedHosts`

View file

@ -100,3 +100,11 @@ rules:
``` ```
After having both resources applied, Traefik will work properly. After having both resources applied, Traefik will work properly.
## v2.1 to v2.2
### Headers middleware: accessControlAllowOrigin
`accessControlAllowOrigin` is deprecated.
This field will be removed in future 2.x releases.
Please configure your allowed origins in `accessControlAllowOriginList` instead.

View file

@ -34,6 +34,7 @@
- "traefik.http.middlewares.middleware10.headers.accesscontrolallowheaders=foobar, foobar" - "traefik.http.middlewares.middleware10.headers.accesscontrolallowheaders=foobar, foobar"
- "traefik.http.middlewares.middleware10.headers.accesscontrolallowmethods=foobar, foobar" - "traefik.http.middlewares.middleware10.headers.accesscontrolallowmethods=foobar, foobar"
- "traefik.http.middlewares.middleware10.headers.accesscontrolalloworigin=foobar" - "traefik.http.middlewares.middleware10.headers.accesscontrolalloworigin=foobar"
- "traefik.http.middlewares.middleware10.headers.accesscontrolalloworiginlist=foobar, foobar"
- "traefik.http.middlewares.middleware10.headers.accesscontrolexposeheaders=foobar, foobar" - "traefik.http.middlewares.middleware10.headers.accesscontrolexposeheaders=foobar, foobar"
- "traefik.http.middlewares.middleware10.headers.accesscontrolmaxage=42" - "traefik.http.middlewares.middleware10.headers.accesscontrolmaxage=42"
- "traefik.http.middlewares.middleware10.headers.addvaryheader=true" - "traefik.http.middlewares.middleware10.headers.addvaryheader=true"
@ -134,6 +135,7 @@
- "traefik.http.routers.router1.tls.domains[1].main=foobar" - "traefik.http.routers.router1.tls.domains[1].main=foobar"
- "traefik.http.routers.router1.tls.domains[1].sans=foobar, foobar" - "traefik.http.routers.router1.tls.domains[1].sans=foobar, foobar"
- "traefik.http.routers.router1.tls.options=foobar" - "traefik.http.routers.router1.tls.options=foobar"
- "traefik.http.services.service01.loadbalancer.healthcheck.followredirects=true"
- "traefik.http.services.service01.loadbalancer.healthcheck.headers.name0=foobar" - "traefik.http.services.service01.loadbalancer.healthcheck.headers.name0=foobar"
- "traefik.http.services.service01.loadbalancer.healthcheck.headers.name1=foobar" - "traefik.http.services.service01.loadbalancer.healthcheck.headers.name1=foobar"
- "traefik.http.services.service01.loadbalancer.healthcheck.hostname=foobar" - "traefik.http.services.service01.loadbalancer.healthcheck.hostname=foobar"

View file

@ -147,6 +147,7 @@
accessControlAllowHeaders = ["foobar", "foobar"] accessControlAllowHeaders = ["foobar", "foobar"]
accessControlAllowMethods = ["foobar", "foobar"] accessControlAllowMethods = ["foobar", "foobar"]
accessControlAllowOrigin = "foobar" accessControlAllowOrigin = "foobar"
accessControlAllowOriginList = ["foobar", "foobar"]
accessControlExposeHeaders = ["foobar", "foobar"] accessControlExposeHeaders = ["foobar", "foobar"]
accessControlMaxAge = 42 accessControlMaxAge = 42
addVaryHeader = true addVaryHeader = true

View file

@ -170,6 +170,9 @@ http:
- foobar - foobar
- foobar - foobar
accessControlAllowOrigin: foobar accessControlAllowOrigin: foobar
accessControlAllowOriginList:
- foobar
- foobar
accessControlExposeHeaders: accessControlExposeHeaders:
- foobar - foobar
- foobar - foobar

View file

@ -41,6 +41,8 @@
| `traefik/http/middlewares/Middleware10/headers/accessControlAllowMethods/0` | `foobar` | | `traefik/http/middlewares/Middleware10/headers/accessControlAllowMethods/0` | `foobar` |
| `traefik/http/middlewares/Middleware10/headers/accessControlAllowMethods/1` | `foobar` | | `traefik/http/middlewares/Middleware10/headers/accessControlAllowMethods/1` | `foobar` |
| `traefik/http/middlewares/Middleware10/headers/accessControlAllowOrigin` | `foobar` | | `traefik/http/middlewares/Middleware10/headers/accessControlAllowOrigin` | `foobar` |
| `traefik/http/middlewares/Middleware10/headers/accessControlAllowOriginList/0` | `foobar` |
| `traefik/http/middlewares/Middleware10/headers/accessControlAllowOriginList/1` | `foobar` |
| `traefik/http/middlewares/Middleware10/headers/accessControlExposeHeaders/0` | `foobar` | | `traefik/http/middlewares/Middleware10/headers/accessControlExposeHeaders/0` | `foobar` |
| `traefik/http/middlewares/Middleware10/headers/accessControlExposeHeaders/1` | `foobar` | | `traefik/http/middlewares/Middleware10/headers/accessControlExposeHeaders/1` | `foobar` |
| `traefik/http/middlewares/Middleware10/headers/accessControlMaxAge` | `42` | | `traefik/http/middlewares/Middleware10/headers/accessControlMaxAge` | `42` |

View file

@ -34,6 +34,7 @@
"traefik.http.middlewares.middleware10.headers.accesscontrolallowheaders": "foobar, foobar", "traefik.http.middlewares.middleware10.headers.accesscontrolallowheaders": "foobar, foobar",
"traefik.http.middlewares.middleware10.headers.accesscontrolallowmethods": "foobar, foobar", "traefik.http.middlewares.middleware10.headers.accesscontrolallowmethods": "foobar, foobar",
"traefik.http.middlewares.middleware10.headers.accesscontrolalloworigin": "foobar", "traefik.http.middlewares.middleware10.headers.accesscontrolalloworigin": "foobar",
"traefik.http.middlewares.middleware10.headers.accesscontrolalloworiginlist": "foobar, foobar",
"traefik.http.middlewares.middleware10.headers.accesscontrolexposeheaders": "foobar, foobar", "traefik.http.middlewares.middleware10.headers.accesscontrolexposeheaders": "foobar, foobar",
"traefik.http.middlewares.middleware10.headers.accesscontrolmaxage": "42", "traefik.http.middlewares.middleware10.headers.accesscontrolmaxage": "42",
"traefik.http.middlewares.middleware10.headers.addvaryheader": "true", "traefik.http.middlewares.middleware10.headers.addvaryheader": "true",
@ -132,6 +133,7 @@
"traefik.http.routers.router1.tls.domains[1].main": "foobar", "traefik.http.routers.router1.tls.domains[1].main": "foobar",
"traefik.http.routers.router1.tls.domains[1].sans": "foobar, foobar", "traefik.http.routers.router1.tls.domains[1].sans": "foobar, foobar",
"traefik.http.routers.router1.tls.options": "foobar", "traefik.http.routers.router1.tls.options": "foobar",
"traefik.http.services.service01.loadbalancer.healthcheck.followredirects": "true",
"traefik.http.services.service01.loadbalancer.healthcheck.headers.name0": "foobar", "traefik.http.services.service01.loadbalancer.healthcheck.headers.name0": "foobar",
"traefik.http.services.service01.loadbalancer.healthcheck.headers.name1": "foobar", "traefik.http.services.service01.loadbalancer.healthcheck.headers.name1": "foobar",
"traefik.http.services.service01.loadbalancer.healthcheck.hostname": "foobar", "traefik.http.services.service01.loadbalancer.healthcheck.hostname": "foobar",

View file

@ -28,7 +28,7 @@
[http.middlewares] [http.middlewares]
[http.middlewares.cors.headers] [http.middlewares.cors.headers]
accessControlAllowMethods= ["GET", "OPTIONS", "PUT"] accessControlAllowMethods= ["GET", "OPTIONS", "PUT"]
accessControlAllowOrigin = "origin-list-or-null" accessControlAllowOriginList = ["https://foo.bar.org"]
accessControlMaxAge = 100 accessControlMaxAge = 100
addVaryHeader = true addVaryHeader = true

View file

@ -367,6 +367,7 @@
accessControlAllowHeaders = ["foobar", "foobar"] accessControlAllowHeaders = ["foobar", "foobar"]
accessControlAllowMethods = ["foobar", "foobar"] accessControlAllowMethods = ["foobar", "foobar"]
accessControlAllowOrigin = "foobar" accessControlAllowOrigin = "foobar"
accessControlAllowOriginList = ["foobar", "foobar"]
accessControlExposeHeaders = ["foobar", "foobar"] accessControlExposeHeaders = ["foobar", "foobar"]
accessControlMaxAge = 42 accessControlMaxAge = 42
addVaryHeader = true addVaryHeader = true

View file

@ -158,7 +158,9 @@ type Headers struct {
// AccessControlAllowMethods must be used in response to a preflight request with Access-Control-Request-Method set. // AccessControlAllowMethods must be used in response to a preflight request with Access-Control-Request-Method set.
AccessControlAllowMethods []string `json:"accessControlAllowMethods,omitempty" toml:"accessControlAllowMethods,omitempty" yaml:"accessControlAllowMethods,omitempty"` AccessControlAllowMethods []string `json:"accessControlAllowMethods,omitempty" toml:"accessControlAllowMethods,omitempty" yaml:"accessControlAllowMethods,omitempty"`
// AccessControlAllowOrigin Can be "origin-list-or-null" or "*". From (https://www.w3.org/TR/cors/#access-control-allow-origin-response-header) // AccessControlAllowOrigin Can be "origin-list-or-null" or "*". From (https://www.w3.org/TR/cors/#access-control-allow-origin-response-header)
AccessControlAllowOrigin string `json:"accessControlAllowOrigin,omitempty" toml:"accessControlAllowOrigin,omitempty" yaml:"accessControlAllowOrigin,omitempty"` AccessControlAllowOrigin string `json:"accessControlAllowOrigin,omitempty" toml:"accessControlAllowOrigin,omitempty" yaml:"accessControlAllowOrigin,omitempty"` // Deprecated
// AccessControlAllowOriginList is a list of allowable origins. Can also be a wildcard origin "*".
AccessControlAllowOriginList []string `json:"accessControlAllowOriginList,omitempty" toml:"accessControlAllowOriginList,omitempty" yaml:"accessControlAllowOriginList,omitempty"`
// AccessControlExposeHeaders sets valid headers for the response. // AccessControlExposeHeaders sets valid headers for the response.
AccessControlExposeHeaders []string `json:"accessControlExposeHeaders,omitempty" toml:"accessControlExposeHeaders,omitempty" yaml:"accessControlExposeHeaders,omitempty"` AccessControlExposeHeaders []string `json:"accessControlExposeHeaders,omitempty" toml:"accessControlExposeHeaders,omitempty" yaml:"accessControlExposeHeaders,omitempty"`
// AccessControlMaxAge sets the time that a preflight request may be cached. // AccessControlMaxAge sets the time that a preflight request may be cached.
@ -200,7 +202,7 @@ func (h *Headers) HasCorsHeadersDefined() bool {
return h != nil && (h.AccessControlAllowCredentials || return h != nil && (h.AccessControlAllowCredentials ||
len(h.AccessControlAllowHeaders) != 0 || len(h.AccessControlAllowHeaders) != 0 ||
len(h.AccessControlAllowMethods) != 0 || len(h.AccessControlAllowMethods) != 0 ||
h.AccessControlAllowOrigin != "" || len(h.AccessControlAllowOriginList) != 0 ||
len(h.AccessControlExposeHeaders) != 0 || len(h.AccessControlExposeHeaders) != 0 ||
h.AccessControlMaxAge != 0 || h.AccessControlMaxAge != 0 ||
h.AddVaryHeader) h.AddVaryHeader)

View file

@ -47,6 +47,7 @@ func TestDecodeConfiguration(t *testing.T) {
"traefik.http.middlewares.Middleware8.headers.accesscontrolallowheaders": "X-foobar, X-fiibar", "traefik.http.middlewares.Middleware8.headers.accesscontrolallowheaders": "X-foobar, X-fiibar",
"traefik.http.middlewares.Middleware8.headers.accesscontrolallowmethods": "GET, PUT", "traefik.http.middlewares.Middleware8.headers.accesscontrolallowmethods": "GET, PUT",
"traefik.http.middlewares.Middleware8.headers.accesscontrolalloworigin": "foobar", "traefik.http.middlewares.Middleware8.headers.accesscontrolalloworigin": "foobar",
"traefik.http.middlewares.Middleware8.headers.accesscontrolalloworiginList": "foobar, fiibar",
"traefik.http.middlewares.Middleware8.headers.accesscontrolexposeheaders": "X-foobar, X-fiibar", "traefik.http.middlewares.Middleware8.headers.accesscontrolexposeheaders": "X-foobar, X-fiibar",
"traefik.http.middlewares.Middleware8.headers.accesscontrolmaxage": "200", "traefik.http.middlewares.Middleware8.headers.accesscontrolmaxage": "200",
"traefik.http.middlewares.Middleware8.headers.addvaryheader": "true", "traefik.http.middlewares.Middleware8.headers.addvaryheader": "true",
@ -516,6 +517,10 @@ func TestDecodeConfiguration(t *testing.T) {
"PUT", "PUT",
}, },
AccessControlAllowOrigin: "foobar", AccessControlAllowOrigin: "foobar",
AccessControlAllowOriginList: []string{
"foobar",
"fiibar",
},
AccessControlExposeHeaders: []string{ AccessControlExposeHeaders: []string{
"X-foobar", "X-foobar",
"X-fiibar", "X-fiibar",
@ -964,6 +969,10 @@ func TestEncodeConfiguration(t *testing.T) {
"PUT", "PUT",
}, },
AccessControlAllowOrigin: "foobar", AccessControlAllowOrigin: "foobar",
AccessControlAllowOriginList: []string{
"foobar",
"fiibar",
},
AccessControlExposeHeaders: []string{ AccessControlExposeHeaders: []string{
"X-foobar", "X-foobar",
"X-fiibar", "X-fiibar",
@ -1118,6 +1127,7 @@ func TestEncodeConfiguration(t *testing.T) {
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowHeaders": "X-foobar, X-fiibar", "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowHeaders": "X-foobar, X-fiibar",
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowMethods": "GET, PUT", "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowMethods": "GET, PUT",
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowOrigin": "foobar", "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowOrigin": "foobar",
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowOriginList": "foobar, fiibar",
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlExposeHeaders": "X-foobar, X-fiibar", "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlExposeHeaders": "X-foobar, X-fiibar",
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlMaxAge": "200", "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlMaxAge": "200",
"traefik.HTTP.Middlewares.Middleware8.Headers.AddVaryHeader": "true", "traefik.HTTP.Middlewares.Middleware8.Headers.AddVaryHeader": "true",

View file

@ -20,20 +20,31 @@ const (
typeName = "Headers" typeName = "Headers"
) )
func handleDeprecation(ctx context.Context, cfg *dynamic.Headers) {
if cfg.AccessControlAllowOrigin != "" {
log.FromContext(ctx).Warn("accessControlAllowOrigin is deprecated, please use accessControlAllowOriginList instead.")
cfg.AccessControlAllowOriginList = append(cfg.AccessControlAllowOriginList, cfg.AccessControlAllowOrigin)
cfg.AccessControlAllowOrigin = ""
}
}
type headers struct { type headers struct {
name string name string
handler http.Handler handler http.Handler
} }
// New creates a Headers middleware. // New creates a Headers middleware.
func New(ctx context.Context, next http.Handler, config dynamic.Headers, name string) (http.Handler, error) { func New(ctx context.Context, next http.Handler, cfg dynamic.Headers, name string) (http.Handler, error) {
// HeaderMiddleware -> SecureMiddleWare -> next // HeaderMiddleware -> SecureMiddleWare -> next
logger := log.FromContext(middlewares.GetLoggerCtx(ctx, name, typeName)) mCtx := middlewares.GetLoggerCtx(ctx, name, typeName)
logger := log.FromContext(mCtx)
logger.Debug("Creating middleware") logger.Debug("Creating middleware")
hasSecureHeaders := config.HasSecureHeadersDefined() handleDeprecation(mCtx, &cfg)
hasCustomHeaders := config.HasCustomHeadersDefined()
hasCorsHeaders := config.HasCorsHeadersDefined() hasSecureHeaders := cfg.HasSecureHeadersDefined()
hasCustomHeaders := cfg.HasCustomHeadersDefined()
hasCorsHeaders := cfg.HasCorsHeadersDefined()
if !hasSecureHeaders && !hasCustomHeaders && !hasCorsHeaders { if !hasSecureHeaders && !hasCustomHeaders && !hasCorsHeaders {
return nil, errors.New("headers configuration not valid") return nil, errors.New("headers configuration not valid")
@ -43,14 +54,14 @@ func New(ctx context.Context, next http.Handler, config dynamic.Headers, name st
nextHandler := next nextHandler := next
if hasSecureHeaders { if hasSecureHeaders {
logger.Debug("Setting up secureHeaders from %v", config) logger.Debug("Setting up secureHeaders from %v", cfg)
handler = newSecure(next, config) handler = newSecure(next, cfg)
nextHandler = handler nextHandler = handler
} }
if hasCustomHeaders || hasCorsHeaders { if hasCustomHeaders || hasCorsHeaders {
logger.Debug("Setting up customHeaders/Cors from %v", config) logger.Debug("Setting up customHeaders/Cors from %v", cfg)
handler = NewHeader(nextHandler, config) handler = NewHeader(nextHandler, cfg)
} }
return &headers{ return &headers{
@ -73,29 +84,29 @@ type secureHeader struct {
} }
// newSecure constructs a new secure instance with supplied options. // newSecure constructs a new secure instance with supplied options.
func newSecure(next http.Handler, headers dynamic.Headers) *secureHeader { func newSecure(next http.Handler, cfg dynamic.Headers) *secureHeader {
opt := secure.Options{ opt := secure.Options{
BrowserXssFilter: headers.BrowserXSSFilter, BrowserXssFilter: cfg.BrowserXSSFilter,
ContentTypeNosniff: headers.ContentTypeNosniff, ContentTypeNosniff: cfg.ContentTypeNosniff,
ForceSTSHeader: headers.ForceSTSHeader, ForceSTSHeader: cfg.ForceSTSHeader,
FrameDeny: headers.FrameDeny, FrameDeny: cfg.FrameDeny,
IsDevelopment: headers.IsDevelopment, IsDevelopment: cfg.IsDevelopment,
SSLRedirect: headers.SSLRedirect, SSLRedirect: cfg.SSLRedirect,
SSLForceHost: headers.SSLForceHost, SSLForceHost: cfg.SSLForceHost,
SSLTemporaryRedirect: headers.SSLTemporaryRedirect, SSLTemporaryRedirect: cfg.SSLTemporaryRedirect,
STSIncludeSubdomains: headers.STSIncludeSubdomains, STSIncludeSubdomains: cfg.STSIncludeSubdomains,
STSPreload: headers.STSPreload, STSPreload: cfg.STSPreload,
ContentSecurityPolicy: headers.ContentSecurityPolicy, ContentSecurityPolicy: cfg.ContentSecurityPolicy,
CustomBrowserXssValue: headers.CustomBrowserXSSValue, CustomBrowserXssValue: cfg.CustomBrowserXSSValue,
CustomFrameOptionsValue: headers.CustomFrameOptionsValue, CustomFrameOptionsValue: cfg.CustomFrameOptionsValue,
PublicKey: headers.PublicKey, PublicKey: cfg.PublicKey,
ReferrerPolicy: headers.ReferrerPolicy, ReferrerPolicy: cfg.ReferrerPolicy,
SSLHost: headers.SSLHost, SSLHost: cfg.SSLHost,
AllowedHosts: headers.AllowedHosts, AllowedHosts: cfg.AllowedHosts,
HostsProxyHeaders: headers.HostsProxyHeaders, HostsProxyHeaders: cfg.HostsProxyHeaders,
SSLProxyHeaders: headers.SSLProxyHeaders, SSLProxyHeaders: cfg.SSLProxyHeaders,
STSSeconds: headers.STSSeconds, STSSeconds: cfg.STSSeconds,
FeaturePolicy: headers.FeaturePolicy, FeaturePolicy: cfg.FeaturePolicy,
} }
return &secureHeader{ return &secureHeader{
@ -119,13 +130,16 @@ type Header struct {
} }
// NewHeader constructs a new header instance from supplied frontend header struct. // NewHeader constructs a new header instance from supplied frontend header struct.
func NewHeader(next http.Handler, headers dynamic.Headers) *Header { func NewHeader(next http.Handler, cfg dynamic.Headers) *Header {
hasCustomHeaders := headers.HasCustomHeadersDefined() hasCustomHeaders := cfg.HasCustomHeadersDefined()
hasCorsHeaders := headers.HasCorsHeadersDefined() hasCorsHeaders := cfg.HasCorsHeadersDefined()
ctx := log.With(context.Background(), log.Str(log.MiddlewareType, typeName))
handleDeprecation(ctx, &cfg)
return &Header{ return &Header{
next: next, next: next,
headers: &headers, headers: &cfg,
hasCustomHeaders: hasCustomHeaders, hasCustomHeaders: hasCustomHeaders,
hasCorsHeaders: hasCorsHeaders, hasCorsHeaders: hasCorsHeaders,
} }
@ -159,29 +173,6 @@ func (s *Header) modifyCustomRequestHeaders(req *http.Request) {
} }
} }
// preRequestModifyCorsResponseHeaders sets during request processing time,
// all the CORS response headers that we already know that are supposed to be set,
// and which do not depend on a later state of the response.
// One notable example of a header that can only be modified later on is "Vary",
// And this is set in the post-response response modifier method
func (s *Header) preRequestModifyCorsResponseHeaders(rw http.ResponseWriter, req *http.Request) {
originHeader := req.Header.Get("Origin")
allowOrigin := s.getAllowOrigin(originHeader)
if allowOrigin != "" {
rw.Header().Set("Access-Control-Allow-Origin", allowOrigin)
}
if s.headers.AccessControlAllowCredentials {
rw.Header().Set("Access-Control-Allow-Credentials", "true")
}
if len(s.headers.AccessControlExposeHeaders) > 0 {
exposeHeaders := strings.Join(s.headers.AccessControlExposeHeaders, ",")
rw.Header().Set("Access-Control-Expose-Headers", exposeHeaders)
}
}
// PostRequestModifyResponseHeaders set or delete response headers. // PostRequestModifyResponseHeaders set or delete response headers.
// This method is called AFTER the response is generated from the backend // This method is called AFTER the response is generated from the backend
// and can merge/override headers from the backend response. // and can merge/override headers from the backend response.
@ -194,6 +185,25 @@ func (s *Header) PostRequestModifyResponseHeaders(res *http.Response) error {
res.Header.Set(header, value) res.Header.Set(header, value)
} }
} }
if res != nil && res.Request != nil {
originHeader := res.Request.Header.Get("Origin")
allowed, match := s.isOriginAllowed(originHeader)
if allowed {
res.Header.Set("Access-Control-Allow-Origin", match)
}
}
if s.headers.AccessControlAllowCredentials {
res.Header.Set("Access-Control-Allow-Credentials", "true")
}
if len(s.headers.AccessControlExposeHeaders) > 0 {
exposeHeaders := strings.Join(s.headers.AccessControlExposeHeaders, ",")
res.Header.Set("Access-Control-Expose-Headers", exposeHeaders)
}
if !s.headers.AddVaryHeader { if !s.headers.AddVaryHeader {
return nil return nil
} }
@ -241,30 +251,24 @@ func (s *Header) processCorsHeaders(rw http.ResponseWriter, req *http.Request) b
rw.Header().Set("Access-Control-Allow-Methods", allowMethods) rw.Header().Set("Access-Control-Allow-Methods", allowMethods)
} }
allowOrigin := s.getAllowOrigin(originHeader) allowed, match := s.isOriginAllowed(originHeader)
if allowed {
if allowOrigin != "" { rw.Header().Set("Access-Control-Allow-Origin", match)
rw.Header().Set("Access-Control-Allow-Origin", allowOrigin)
} }
rw.Header().Set("Access-Control-Max-Age", strconv.Itoa(int(s.headers.AccessControlMaxAge))) rw.Header().Set("Access-Control-Max-Age", strconv.Itoa(int(s.headers.AccessControlMaxAge)))
return true return true
} }
s.preRequestModifyCorsResponseHeaders(rw, req)
return false return false
} }
func (s *Header) getAllowOrigin(header string) string { func (s *Header) isOriginAllowed(origin string) (bool, string) {
switch s.headers.AccessControlAllowOrigin { for _, item := range s.headers.AccessControlAllowOriginList {
case "origin-list-or-null": if item == "*" || item == origin {
if len(header) == 0 { return true, item
return "null"
} }
return header
case "*":
return "*"
default:
return ""
} }
return false, ""
} }

View file

@ -202,9 +202,9 @@ func TestCORSPreflights(t *testing.T) {
{ {
desc: "Test Simple Preflight", desc: "Test Simple Preflight",
header: NewHeader(emptyHandler, dynamic.Headers{ header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"}, AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
AccessControlAllowOrigin: "origin-list-or-null", AccessControlAllowOriginList: []string{"https://foo.bar.org"},
AccessControlMaxAge: 600, AccessControlMaxAge: 600,
}), }),
requestHeaders: map[string][]string{ requestHeaders: map[string][]string{
"Access-Control-Request-Headers": {"origin"}, "Access-Control-Request-Headers": {"origin"},
@ -220,9 +220,9 @@ func TestCORSPreflights(t *testing.T) {
{ {
desc: "Wildcard origin Preflight", desc: "Wildcard origin Preflight",
header: NewHeader(emptyHandler, dynamic.Headers{ header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"}, AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
AccessControlAllowOrigin: "*", AccessControlAllowOriginList: []string{"*"},
AccessControlMaxAge: 600, AccessControlMaxAge: 600,
}), }),
requestHeaders: map[string][]string{ requestHeaders: map[string][]string{
"Access-Control-Request-Headers": {"origin"}, "Access-Control-Request-Headers": {"origin"},
@ -239,7 +239,7 @@ func TestCORSPreflights(t *testing.T) {
desc: "Allow Credentials Preflight", desc: "Allow Credentials Preflight",
header: NewHeader(emptyHandler, dynamic.Headers{ header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"}, AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
AccessControlAllowOrigin: "*", AccessControlAllowOriginList: []string{"*"},
AccessControlAllowCredentials: true, AccessControlAllowCredentials: true,
AccessControlMaxAge: 600, AccessControlMaxAge: 600,
}), }),
@ -258,10 +258,10 @@ func TestCORSPreflights(t *testing.T) {
{ {
desc: "Allow Headers Preflight", desc: "Allow Headers Preflight",
header: NewHeader(emptyHandler, dynamic.Headers{ header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"}, AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
AccessControlAllowOrigin: "*", AccessControlAllowOriginList: []string{"*"},
AccessControlAllowHeaders: []string{"origin", "X-Forwarded-For"}, AccessControlAllowHeaders: []string{"origin", "X-Forwarded-For"},
AccessControlMaxAge: 600, AccessControlMaxAge: 600,
}), }),
requestHeaders: map[string][]string{ requestHeaders: map[string][]string{
"Access-Control-Request-Headers": {"origin"}, "Access-Control-Request-Headers": {"origin"},
@ -278,10 +278,10 @@ func TestCORSPreflights(t *testing.T) {
{ {
desc: "No Request Headers Preflight", desc: "No Request Headers Preflight",
header: NewHeader(emptyHandler, dynamic.Headers{ header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"}, AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
AccessControlAllowOrigin: "*", AccessControlAllowOriginList: []string{"*"},
AccessControlAllowHeaders: []string{"origin", "X-Forwarded-For"}, AccessControlAllowHeaders: []string{"origin", "X-Forwarded-For"},
AccessControlMaxAge: 600, AccessControlMaxAge: 600,
}), }),
requestHeaders: map[string][]string{ requestHeaders: map[string][]string{
"Access-Control-Request-Method": {"GET", "OPTIONS"}, "Access-Control-Request-Method": {"GET", "OPTIONS"},
@ -352,6 +352,12 @@ func TestCORSResponses(t *testing.T) {
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
nonEmptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Vary", "Testing") }) nonEmptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Vary", "Testing") })
existingOriginHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Vary", "Origin") }) existingOriginHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Vary", "Origin") })
existingAccessControlAllowOriginHandlerSet := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "http://foo.bar.org")
})
existingAccessControlAllowOriginHandlerAdd := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "http://foo.bar.org")
})
testCases := []struct { testCases := []struct {
desc string desc string
@ -362,7 +368,7 @@ func TestCORSResponses(t *testing.T) {
{ {
desc: "Test Simple Request", desc: "Test Simple Request",
header: NewHeader(emptyHandler, dynamic.Headers{ header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowOrigin: "origin-list-or-null", AccessControlAllowOriginList: []string{"https://foo.bar.org"},
}), }),
requestHeaders: map[string][]string{ requestHeaders: map[string][]string{
"Origin": {"https://foo.bar.org"}, "Origin": {"https://foo.bar.org"},
@ -374,7 +380,7 @@ func TestCORSResponses(t *testing.T) {
{ {
desc: "Wildcard origin Request", desc: "Wildcard origin Request",
header: NewHeader(emptyHandler, dynamic.Headers{ header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowOrigin: "*", AccessControlAllowOriginList: []string{"*"},
}), }),
requestHeaders: map[string][]string{ requestHeaders: map[string][]string{
"Origin": {"https://foo.bar.org"}, "Origin": {"https://foo.bar.org"},
@ -386,12 +392,10 @@ func TestCORSResponses(t *testing.T) {
{ {
desc: "Empty origin Request", desc: "Empty origin Request",
header: NewHeader(emptyHandler, dynamic.Headers{ header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowOrigin: "origin-list-or-null", AccessControlAllowOriginList: []string{"https://foo.bar.org"},
}), }),
requestHeaders: map[string][]string{}, requestHeaders: map[string][]string{},
expected: map[string][]string{ expected: map[string][]string{},
"Access-Control-Allow-Origin": {"null"},
},
}, },
{ {
desc: "Not Defined origin Request", desc: "Not Defined origin Request",
@ -402,7 +406,7 @@ func TestCORSResponses(t *testing.T) {
{ {
desc: "Allow Credentials Request", desc: "Allow Credentials Request",
header: NewHeader(emptyHandler, dynamic.Headers{ header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowOrigin: "*", AccessControlAllowOriginList: []string{"*"},
AccessControlAllowCredentials: true, AccessControlAllowCredentials: true,
}), }),
requestHeaders: map[string][]string{ requestHeaders: map[string][]string{
@ -416,8 +420,8 @@ func TestCORSResponses(t *testing.T) {
{ {
desc: "Expose Headers Request", desc: "Expose Headers Request",
header: NewHeader(emptyHandler, dynamic.Headers{ header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowOrigin: "*", AccessControlAllowOriginList: []string{"*"},
AccessControlExposeHeaders: []string{"origin", "X-Forwarded-For"}, AccessControlExposeHeaders: []string{"origin", "X-Forwarded-For"},
}), }),
requestHeaders: map[string][]string{ requestHeaders: map[string][]string{
"Origin": {"https://foo.bar.org"}, "Origin": {"https://foo.bar.org"},
@ -430,8 +434,8 @@ func TestCORSResponses(t *testing.T) {
{ {
desc: "Test Simple Request with Vary Headers", desc: "Test Simple Request with Vary Headers",
header: NewHeader(emptyHandler, dynamic.Headers{ header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowOrigin: "origin-list-or-null", AccessControlAllowOriginList: []string{"https://foo.bar.org"},
AddVaryHeader: true, AddVaryHeader: true,
}), }),
requestHeaders: map[string][]string{ requestHeaders: map[string][]string{
"Origin": {"https://foo.bar.org"}, "Origin": {"https://foo.bar.org"},
@ -444,8 +448,8 @@ func TestCORSResponses(t *testing.T) {
{ {
desc: "Test Simple Request with Vary Headers and non-empty response", desc: "Test Simple Request with Vary Headers and non-empty response",
header: NewHeader(nonEmptyHandler, dynamic.Headers{ header: NewHeader(nonEmptyHandler, dynamic.Headers{
AccessControlAllowOrigin: "origin-list-or-null", AccessControlAllowOriginList: []string{"https://foo.bar.org"},
AddVaryHeader: true, AddVaryHeader: true,
}), }),
requestHeaders: map[string][]string{ requestHeaders: map[string][]string{
"Origin": {"https://foo.bar.org"}, "Origin": {"https://foo.bar.org"},
@ -458,8 +462,8 @@ func TestCORSResponses(t *testing.T) {
{ {
desc: "Test Simple Request with Vary Headers and existing vary:origin response", desc: "Test Simple Request with Vary Headers and existing vary:origin response",
header: NewHeader(existingOriginHandler, dynamic.Headers{ header: NewHeader(existingOriginHandler, dynamic.Headers{
AccessControlAllowOrigin: "origin-list-or-null", AccessControlAllowOriginList: []string{"https://foo.bar.org"},
AddVaryHeader: true, AddVaryHeader: true,
}), }),
requestHeaders: map[string][]string{ requestHeaders: map[string][]string{
"Origin": {"https://foo.bar.org"}, "Origin": {"https://foo.bar.org"},
@ -470,6 +474,29 @@ func TestCORSResponses(t *testing.T) {
}, },
}, },
{ {
desc: "Test Simple Request with non-empty response: set ACAO",
header: NewHeader(existingAccessControlAllowOriginHandlerSet, dynamic.Headers{
AccessControlAllowOriginList: []string{"*"},
}),
requestHeaders: map[string][]string{
"Origin": {"https://foo.bar.org"},
},
expected: map[string][]string{
"Access-Control-Allow-Origin": {"*"},
},
},
{
desc: "Test Simple Request with non-empty response: add ACAO",
header: NewHeader(existingAccessControlAllowOriginHandlerAdd, dynamic.Headers{
AccessControlAllowOriginList: []string{"*"},
}),
requestHeaders: map[string][]string{
"Origin": {"https://foo.bar.org"},
},
expected: map[string][]string{
"Access-Control-Allow-Origin": {"*"},
},
}, {
desc: "Test Simple CustomRequestHeaders Not Hijacked by CORS", desc: "Test Simple CustomRequestHeaders Not Hijacked by CORS",
header: NewHeader(emptyHandler, dynamic.Headers{ header: NewHeader(emptyHandler, dynamic.Headers{
CustomRequestHeaders: map[string]string{"foo": "bar"}, CustomRequestHeaders: map[string]string{"foo": "bar"},
@ -487,10 +514,11 @@ func TestCORSResponses(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
req := testhelpers.MustNewRequest(http.MethodGet, "/foo", nil) req := testhelpers.MustNewRequest(http.MethodGet, "/foo", nil)
req.Header = test.requestHeaders req.Header = test.requestHeaders
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
test.header.ServeHTTP(rw, req) test.header.ServeHTTP(rw, req)
err := test.header.PostRequestModifyResponseHeaders(rw.Result()) res := rw.Result()
res.Request = req
err := test.header.PostRequestModifyResponseHeaders(res)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, test.expected, rw.Result().Header) assert.Equal(t, test.expected, rw.Result().Header)
}) })

View file

@ -94,6 +94,8 @@ func Test_buildConfiguration(t *testing.T) {
"traefik/http/middlewares/Middleware09/headers/accessControlAllowHeaders/0": "foobar", "traefik/http/middlewares/Middleware09/headers/accessControlAllowHeaders/0": "foobar",
"traefik/http/middlewares/Middleware09/headers/accessControlAllowHeaders/1": "foobar", "traefik/http/middlewares/Middleware09/headers/accessControlAllowHeaders/1": "foobar",
"traefik/http/middlewares/Middleware09/headers/accessControlAllowOrigin": "foobar", "traefik/http/middlewares/Middleware09/headers/accessControlAllowOrigin": "foobar",
"traefik/http/middlewares/Middleware09/headers/accessControlAllowOriginList/0": "foobar",
"traefik/http/middlewares/Middleware09/headers/accessControlAllowOriginList/1": "foobar",
"traefik/http/middlewares/Middleware09/headers/contentTypeNosniff": "true", "traefik/http/middlewares/Middleware09/headers/contentTypeNosniff": "true",
"traefik/http/middlewares/Middleware09/headers/accessControlAllowCredentials": "true", "traefik/http/middlewares/Middleware09/headers/accessControlAllowCredentials": "true",
"traefik/http/middlewares/Middleware09/headers/featurePolicy": "foobar", "traefik/http/middlewares/Middleware09/headers/featurePolicy": "foobar",
@ -543,6 +545,10 @@ func Test_buildConfiguration(t *testing.T) {
"foobar", "foobar",
}, },
AccessControlAllowOrigin: "foobar", AccessControlAllowOrigin: "foobar",
AccessControlAllowOriginList: []string{
"foobar",
"foobar",
},
AccessControlExposeHeaders: []string{ AccessControlExposeHeaders: []string{
"foobar", "foobar",
"foobar", "foobar",