From b5198e63c446efdb488abd2bea826a59f75b1d63 Mon Sep 17 00:00:00 2001 From: Luca Guidi Date: Thu, 29 Oct 2020 10:52:03 +0100 Subject: [PATCH] Allow to use regular expressions for `AccessControlAllowOriginList` --- docs/content/middlewares/headers.md | 10 ++- .../dynamic-configuration/docker-labels.yml | 1 + .../reference/dynamic-configuration/file.toml | 1 + .../reference/dynamic-configuration/file.yaml | 3 + .../reference/dynamic-configuration/kv-ref.md | 2 + pkg/config/dynamic/middlewares.go | 3 + pkg/config/label/label_test.go | 10 +++ pkg/middlewares/headers/header.go | 39 ++++++++--- pkg/middlewares/headers/header_test.go | 69 +++++++++++++++++-- pkg/middlewares/headers/headers.go | 6 +- pkg/provider/kv/kv_test.go | 6 ++ 11 files changed, 133 insertions(+), 17 deletions(-) diff --git a/docs/content/middlewares/headers.md b/docs/content/middlewares/headers.md index 70e51cf54..4018382a1 100644 --- a/docs/content/middlewares/headers.md +++ b/docs/content/middlewares/headers.md @@ -306,7 +306,7 @@ The `accessControlAllowOriginList` indicates whether a resource can be shared by 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 -This value can contains a list of allowed origins. +This value can contain a list of allowed origins. More information including how to use the settings can be found on: @@ -316,6 +316,14 @@ More information including how to use the settings can be found on: 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). +### `accessControlAllowOriginListRegex` + +The `accessControlAllowOriginListRegex` option is the counterpart of the `accessControlAllowOriginList` option with regular expressions instead of origin values. +It will allow all origin that contains any match of a regular expression in the `accessControlAllowOriginList`. + +!!! tip + Regular expressions can be tested using online tools such as [Go Playground](https://play.golang.org/p/mWU9p-wk2ru) or the [Regex101](https://regex101.com/r/58sIgx/2). + ### `accessControlExposeHeaders` The `accessControlExposeHeaders` indicates which headers are safe to expose to the api of a CORS API specification. diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index fd4ef1113..080141f1a 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -36,6 +36,7 @@ - "traefik.http.middlewares.middleware10.headers.accesscontrolallowmethods=foobar, foobar" - "traefik.http.middlewares.middleware10.headers.accesscontrolalloworigin=foobar" - "traefik.http.middlewares.middleware10.headers.accesscontrolalloworiginlist=foobar, foobar" +- "traefik.http.middlewares.middleware10.headers.accesscontrolalloworiginlistregex=foobar, foobar" - "traefik.http.middlewares.middleware10.headers.accesscontrolexposeheaders=foobar, foobar" - "traefik.http.middlewares.middleware10.headers.accesscontrolmaxage=42" - "traefik.http.middlewares.middleware10.headers.addvaryheader=true" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 67ebefdf7..2fc955ba8 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -153,6 +153,7 @@ accessControlAllowMethods = ["foobar", "foobar"] accessControlAllowOrigin = "foobar" accessControlAllowOriginList = ["foobar", "foobar"] + accessControlAllowOriginListRegex = ["foobar", "foobar"] accessControlExposeHeaders = ["foobar", "foobar"] accessControlMaxAge = 42 addVaryHeader = true diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 5f8f36f51..08bc53795 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -180,6 +180,9 @@ http: accessControlAllowOriginList: - foobar - foobar + accessControlAllowOriginListRegex: + - foobar + - foobar accessControlExposeHeaders: - foobar - foobar diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index ec7bdcbf9..2e6c012ff 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -45,6 +45,8 @@ | `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/accessControlAllowOriginListRegex/0` | `foobar` | +| `traefik/http/middlewares/Middleware10/headers/accessControlAllowOriginListRegex/1` | `foobar` | | `traefik/http/middlewares/Middleware10/headers/accessControlExposeHeaders/0` | `foobar` | | `traefik/http/middlewares/Middleware10/headers/accessControlExposeHeaders/1` | `foobar` | | `traefik/http/middlewares/Middleware10/headers/accessControlMaxAge` | `42` | diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index 57fee2f42..eec7ea88a 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -164,6 +164,8 @@ type Headers struct { 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"` + // AccessControlAllowOriginListRegex is a list of allowable origins written following the Regular Expression syntax (https://golang.org/pkg/regexp/). + AccessControlAllowOriginListRegex []string `json:"accessControlAllowOriginListRegex,omitempty" toml:"accessControlAllowOriginListRegex,omitempty" yaml:"accessControlAllowOriginListRegex,omitempty"` // AccessControlExposeHeaders sets valid headers for the response. AccessControlExposeHeaders []string `json:"accessControlExposeHeaders,omitempty" toml:"accessControlExposeHeaders,omitempty" yaml:"accessControlExposeHeaders,omitempty"` // AccessControlMaxAge sets the time that a preflight request may be cached. @@ -206,6 +208,7 @@ func (h *Headers) HasCorsHeadersDefined() bool { len(h.AccessControlAllowHeaders) != 0 || len(h.AccessControlAllowMethods) != 0 || len(h.AccessControlAllowOriginList) != 0 || + len(h.AccessControlAllowOriginListRegex) != 0 || len(h.AccessControlExposeHeaders) != 0 || h.AccessControlMaxAge != 0 || h.AddVaryHeader) diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index 54f9c5358..319af7d8c 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -49,6 +49,7 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.middlewares.Middleware8.headers.accesscontrolallowmethods": "GET, PUT", "traefik.http.middlewares.Middleware8.headers.accesscontrolalloworigin": "foobar", "traefik.http.middlewares.Middleware8.headers.accesscontrolalloworiginList": "foobar, fiibar", + "traefik.http.middlewares.Middleware8.headers.accesscontrolalloworiginListRegex": "foobar, fiibar", "traefik.http.middlewares.Middleware8.headers.accesscontrolexposeheaders": "X-foobar, X-fiibar", "traefik.http.middlewares.Middleware8.headers.accesscontrolmaxage": "200", "traefik.http.middlewares.Middleware8.headers.addvaryheader": "true", @@ -527,6 +528,10 @@ func TestDecodeConfiguration(t *testing.T) { "foobar", "fiibar", }, + AccessControlAllowOriginListRegex: []string{ + "foobar", + "fiibar", + }, AccessControlExposeHeaders: []string{ "X-foobar", "X-fiibar", @@ -999,6 +1004,10 @@ func TestEncodeConfiguration(t *testing.T) { "foobar", "fiibar", }, + AccessControlAllowOriginListRegex: []string{ + "foobar", + "fiibar", + }, AccessControlExposeHeaders: []string{ "X-foobar", "X-fiibar", @@ -1155,6 +1164,7 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowMethods": "GET, PUT", "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowOrigin": "foobar", "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowOriginList": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowOriginListRegex": "foobar, fiibar", "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlExposeHeaders": "X-foobar, X-fiibar", "traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlMaxAge": "200", "traefik.HTTP.Middlewares.Middleware8.Headers.AddVaryHeader": "true", diff --git a/pkg/middlewares/headers/header.go b/pkg/middlewares/headers/header.go index 0a17e0cce..7c5a99af5 100644 --- a/pkg/middlewares/headers/header.go +++ b/pkg/middlewares/headers/header.go @@ -2,7 +2,9 @@ package headers import ( "context" + "fmt" "net/http" + "regexp" "strconv" "strings" @@ -14,26 +16,37 @@ import ( // A single headerOptions struct can be provided to configure which features should be enabled, // and the ability to override a few of the default values. type Header struct { - next http.Handler - hasCustomHeaders bool - hasCorsHeaders bool - headers *dynamic.Headers + next http.Handler + hasCustomHeaders bool + hasCorsHeaders bool + headers *dynamic.Headers + allowOriginRegexes []*regexp.Regexp } // NewHeader constructs a new header instance from supplied frontend header struct. -func NewHeader(next http.Handler, cfg dynamic.Headers) *Header { +func NewHeader(next http.Handler, cfg dynamic.Headers) (*Header, error) { hasCustomHeaders := cfg.HasCustomHeadersDefined() hasCorsHeaders := cfg.HasCorsHeadersDefined() ctx := log.With(context.Background(), log.Str(log.MiddlewareType, typeName)) handleDeprecation(ctx, &cfg) - return &Header{ - next: next, - headers: &cfg, - hasCustomHeaders: hasCustomHeaders, - hasCorsHeaders: hasCorsHeaders, + regexes := make([]*regexp.Regexp, len(cfg.AccessControlAllowOriginListRegex)) + for i, str := range cfg.AccessControlAllowOriginListRegex { + reg, err := regexp.Compile(str) + if err != nil { + return nil, fmt.Errorf("error occurred during origin parsing: %w", err) + } + regexes[i] = reg } + + return &Header{ + next: next, + headers: &cfg, + hasCustomHeaders: hasCustomHeaders, + hasCorsHeaders: hasCorsHeaders, + allowOriginRegexes: regexes, + }, nil } func (s *Header) ServeHTTP(rw http.ResponseWriter, req *http.Request) { @@ -166,5 +179,11 @@ func (s *Header) isOriginAllowed(origin string) (bool, string) { } } + for _, regex := range s.allowOriginRegexes { + if regex.MatchString(origin) { + return true, origin + } + } + return false, "" } diff --git a/pkg/middlewares/headers/header_test.go b/pkg/middlewares/headers/header_test.go index 7e7cb9b6a..fa21fe311 100644 --- a/pkg/middlewares/headers/header_test.go +++ b/pkg/middlewares/headers/header_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/traefik/traefik/v2/pkg/config/dynamic" ) @@ -52,7 +53,8 @@ func TestNewHeader_customRequestHeader(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - mid := NewHeader(emptyHandler, test.cfg) + mid, err := NewHeader(emptyHandler, test.cfg) + require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/foo", nil) req.Header.Set("Foo", "bar") @@ -94,7 +96,8 @@ func TestNewHeader_customRequestHeader_Host(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - mid := NewHeader(emptyHandler, dynamic.Headers{CustomRequestHeaders: test.customHeaders}) + mid, err := NewHeader(emptyHandler, dynamic.Headers{CustomRequestHeaders: test.customHeaders}) + require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "http://example.org/foo", nil) @@ -217,7 +220,8 @@ func TestNewHeader_CORSPreflights(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - mid := NewHeader(emptyHandler, test.cfg) + mid, err := NewHeader(emptyHandler, test.cfg) + require.NoError(t, err) req := httptest.NewRequest(http.MethodOptions, "/foo", nil) req.Header = test.requestHeaders @@ -240,6 +244,7 @@ func TestNewHeader_CORSResponses(t *testing.T) { cfg dynamic.Headers requestHeaders http.Header expected http.Header + expectedError bool }{ { desc: "Test Simple Request", @@ -267,6 +272,54 @@ func TestNewHeader_CORSResponses(t *testing.T) { "Access-Control-Allow-Origin": {"*"}, }, }, + { + desc: "Regexp Origin Request", + next: emptyHandler, + cfg: dynamic.Headers{ + AccessControlAllowOriginListRegex: []string{"^https?://([a-z]+)\\.bar\\.org$"}, + }, + requestHeaders: map[string][]string{ + "Origin": {"https://foo.bar.org"}, + }, + expected: map[string][]string{ + "Access-Control-Allow-Origin": {"https://foo.bar.org"}, + }, + }, + { + desc: "Partial Regexp Origin Request", + next: emptyHandler, + cfg: dynamic.Headers{ + AccessControlAllowOriginListRegex: []string{"([a-z]+)\\.bar"}, + }, + requestHeaders: map[string][]string{ + "Origin": {"https://foo.bar.org"}, + }, + expected: map[string][]string{ + "Access-Control-Allow-Origin": {"https://foo.bar.org"}, + }, + }, + { + desc: "Regexp Malformed Origin Request", + next: emptyHandler, + cfg: dynamic.Headers{ + AccessControlAllowOriginListRegex: []string{"a(b"}, + }, + requestHeaders: map[string][]string{ + "Origin": {"https://foo.bar.org"}, + }, + expectedError: true, + }, + { + desc: "Regexp Origin Request without matching", + next: emptyHandler, + cfg: dynamic.Headers{ + AccessControlAllowOriginListRegex: []string{"([a-z]+)\\.bar\\.org"}, + }, + requestHeaders: map[string][]string{ + "Origin": {"https://bar.org"}, + }, + expected: map[string][]string{}, + }, { desc: "Empty origin Request", next: emptyHandler, @@ -416,7 +469,12 @@ func TestNewHeader_CORSResponses(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - mid := NewHeader(test.next, test.cfg) + mid, err := NewHeader(test.next, test.cfg) + if test.expectedError { + require.Error(t, err) + return + } + require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/foo", nil) req.Header = test.requestHeaders @@ -478,7 +536,8 @@ func TestNewHeader_customResponseHeaders(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - mid := NewHeader(emptyHandler, dynamic.Headers{CustomResponseHeaders: test.config}) + mid, err := NewHeader(emptyHandler, dynamic.Headers{CustomResponseHeaders: test.config}) + require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/foo", nil) diff --git a/pkg/middlewares/headers/headers.go b/pkg/middlewares/headers/headers.go index 5f98dbead..39594494a 100644 --- a/pkg/middlewares/headers/headers.go +++ b/pkg/middlewares/headers/headers.go @@ -58,7 +58,11 @@ func New(ctx context.Context, next http.Handler, cfg dynamic.Headers, name strin if hasCustomHeaders || hasCorsHeaders { logger.Debugf("Setting up customHeaders/Cors from %v", cfg) - handler = NewHeader(nextHandler, cfg) + var err error + handler, err = NewHeader(nextHandler, cfg) + if err != nil { + return nil, err + } } return &headers{ diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go index 4d3c5f16f..a6f3d4cb5 100644 --- a/pkg/provider/kv/kv_test.go +++ b/pkg/provider/kv/kv_test.go @@ -100,6 +100,8 @@ func Test_buildConfiguration(t *testing.T) { "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/accessControlAllowOriginListRegex/0": "foobar", + "traefik/http/middlewares/Middleware09/headers/accessControlAllowOriginListRegex/1": "foobar", "traefik/http/middlewares/Middleware09/headers/contentTypeNosniff": "true", "traefik/http/middlewares/Middleware09/headers/accessControlAllowCredentials": "true", "traefik/http/middlewares/Middleware09/headers/featurePolicy": "foobar", @@ -557,6 +559,10 @@ func Test_buildConfiguration(t *testing.T) { "foobar", "foobar", }, + AccessControlAllowOriginListRegex: []string{ + "foobar", + "foobar", + }, AccessControlExposeHeaders: []string{ "foobar", "foobar",