Allow to use regular expressions for AccessControlAllowOriginList

This commit is contained in:
Luca Guidi 2020-10-29 10:52:03 +01:00 committed by GitHub
parent 699cf71652
commit b5198e63c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 133 additions and 17 deletions

View file

@ -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. 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 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: 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). 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` ### `accessControlExposeHeaders`
The `accessControlExposeHeaders` indicates which headers are safe to expose to the api of a CORS API specification. The `accessControlExposeHeaders` indicates which headers are safe to expose to the api of a CORS API specification.

View file

@ -36,6 +36,7 @@
- "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.accesscontrolalloworiginlist=foobar, foobar"
- "traefik.http.middlewares.middleware10.headers.accesscontrolalloworiginlistregex=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"

View file

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

View file

@ -180,6 +180,9 @@ http:
accessControlAllowOriginList: accessControlAllowOriginList:
- foobar - foobar
- foobar - foobar
accessControlAllowOriginListRegex:
- foobar
- foobar
accessControlExposeHeaders: accessControlExposeHeaders:
- foobar - foobar
- foobar - foobar

View file

@ -45,6 +45,8 @@
| `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/0` | `foobar` |
| `traefik/http/middlewares/Middleware10/headers/accessControlAllowOriginList/1` | `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/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

@ -164,6 +164,8 @@ type Headers struct {
AccessControlAllowOrigin string `json:"accessControlAllowOrigin,omitempty" toml:"accessControlAllowOrigin,omitempty" yaml:"accessControlAllowOrigin,omitempty"` // Deprecated 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 is a list of allowable origins. Can also be a wildcard origin "*".
AccessControlAllowOriginList []string `json:"accessControlAllowOriginList,omitempty" toml:"accessControlAllowOriginList,omitempty" yaml:"accessControlAllowOriginList,omitempty"` 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 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.
@ -206,6 +208,7 @@ func (h *Headers) HasCorsHeadersDefined() bool {
len(h.AccessControlAllowHeaders) != 0 || len(h.AccessControlAllowHeaders) != 0 ||
len(h.AccessControlAllowMethods) != 0 || len(h.AccessControlAllowMethods) != 0 ||
len(h.AccessControlAllowOriginList) != 0 || len(h.AccessControlAllowOriginList) != 0 ||
len(h.AccessControlAllowOriginListRegex) != 0 ||
len(h.AccessControlExposeHeaders) != 0 || len(h.AccessControlExposeHeaders) != 0 ||
h.AccessControlMaxAge != 0 || h.AccessControlMaxAge != 0 ||
h.AddVaryHeader) h.AddVaryHeader)

View file

@ -49,6 +49,7 @@ func TestDecodeConfiguration(t *testing.T) {
"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.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.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",
@ -527,6 +528,10 @@ func TestDecodeConfiguration(t *testing.T) {
"foobar", "foobar",
"fiibar", "fiibar",
}, },
AccessControlAllowOriginListRegex: []string{
"foobar",
"fiibar",
},
AccessControlExposeHeaders: []string{ AccessControlExposeHeaders: []string{
"X-foobar", "X-foobar",
"X-fiibar", "X-fiibar",
@ -999,6 +1004,10 @@ func TestEncodeConfiguration(t *testing.T) {
"foobar", "foobar",
"fiibar", "fiibar",
}, },
AccessControlAllowOriginListRegex: []string{
"foobar",
"fiibar",
},
AccessControlExposeHeaders: []string{ AccessControlExposeHeaders: []string{
"X-foobar", "X-foobar",
"X-fiibar", "X-fiibar",
@ -1155,6 +1164,7 @@ func TestEncodeConfiguration(t *testing.T) {
"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.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.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

@ -2,7 +2,9 @@ package headers
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"regexp"
"strconv" "strconv"
"strings" "strings"
@ -18,22 +20,33 @@ type Header struct {
hasCustomHeaders bool hasCustomHeaders bool
hasCorsHeaders bool hasCorsHeaders bool
headers *dynamic.Headers headers *dynamic.Headers
allowOriginRegexes []*regexp.Regexp
} }
// 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, cfg dynamic.Headers) *Header { func NewHeader(next http.Handler, cfg dynamic.Headers) (*Header, error) {
hasCustomHeaders := cfg.HasCustomHeadersDefined() hasCustomHeaders := cfg.HasCustomHeadersDefined()
hasCorsHeaders := cfg.HasCorsHeadersDefined() hasCorsHeaders := cfg.HasCorsHeadersDefined()
ctx := log.With(context.Background(), log.Str(log.MiddlewareType, typeName)) ctx := log.With(context.Background(), log.Str(log.MiddlewareType, typeName))
handleDeprecation(ctx, &cfg) handleDeprecation(ctx, &cfg)
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{ return &Header{
next: next, next: next,
headers: &cfg, headers: &cfg,
hasCustomHeaders: hasCustomHeaders, hasCustomHeaders: hasCustomHeaders,
hasCorsHeaders: hasCorsHeaders, hasCorsHeaders: hasCorsHeaders,
} allowOriginRegexes: regexes,
}, nil
} }
func (s *Header) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 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, "" return false, ""
} }

View file

@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v2/pkg/config/dynamic" "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.Run(test.desc, func(t *testing.T) {
t.Parallel() 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 := httptest.NewRequest(http.MethodGet, "/foo", nil)
req.Header.Set("Foo", "bar") req.Header.Set("Foo", "bar")
@ -94,7 +96,8 @@ func TestNewHeader_customRequestHeader_Host(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { 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) req := httptest.NewRequest(http.MethodGet, "http://example.org/foo", nil)
@ -217,7 +220,8 @@ func TestNewHeader_CORSPreflights(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { 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 := httptest.NewRequest(http.MethodOptions, "/foo", nil)
req.Header = test.requestHeaders req.Header = test.requestHeaders
@ -240,6 +244,7 @@ func TestNewHeader_CORSResponses(t *testing.T) {
cfg dynamic.Headers cfg dynamic.Headers
requestHeaders http.Header requestHeaders http.Header
expected http.Header expected http.Header
expectedError bool
}{ }{
{ {
desc: "Test Simple Request", desc: "Test Simple Request",
@ -267,6 +272,54 @@ func TestNewHeader_CORSResponses(t *testing.T) {
"Access-Control-Allow-Origin": {"*"}, "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", desc: "Empty origin Request",
next: emptyHandler, next: emptyHandler,
@ -416,7 +469,12 @@ func TestNewHeader_CORSResponses(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { 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 := httptest.NewRequest(http.MethodGet, "/foo", nil)
req.Header = test.requestHeaders req.Header = test.requestHeaders
@ -478,7 +536,8 @@ func TestNewHeader_customResponseHeaders(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { 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) req := httptest.NewRequest(http.MethodGet, "/foo", nil)

View file

@ -58,7 +58,11 @@ func New(ctx context.Context, next http.Handler, cfg dynamic.Headers, name strin
if hasCustomHeaders || hasCorsHeaders { if hasCustomHeaders || hasCorsHeaders {
logger.Debugf("Setting up customHeaders/Cors from %v", cfg) 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{ return &headers{

View file

@ -100,6 +100,8 @@ func Test_buildConfiguration(t *testing.T) {
"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/0": "foobar",
"traefik/http/middlewares/Middleware09/headers/accessControlAllowOriginList/1": "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/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",
@ -557,6 +559,10 @@ func Test_buildConfiguration(t *testing.T) {
"foobar", "foobar",
"foobar", "foobar",
}, },
AccessControlAllowOriginListRegex: []string{
"foobar",
"foobar",
},
AccessControlExposeHeaders: []string{ AccessControlExposeHeaders: []string{
"foobar", "foobar",
"foobar", "foobar",