4 Commits

Author SHA1 Message Date
Thom Seddon
62cd687924 Validate redirect domain
This change introduces a validation step prior to redirect as
discussed in #77
2020-04-16 21:23:28 +01:00
Pierre Kisters
c3b4ba8244 Allow multiple cookie domains, domains and whitelists with environment variable (#98)
* comma env-delim for array flags
* tests for env-delim flags
2020-04-14 07:48:55 +01:00
Thiago Pinto
b413c60d42 Update golang arm versions 2020-04-13 18:03:46 +01:00
Sandro Jäckel
e678a33016 Add .git to .dockerignore 2020-04-13 18:01:07 +01:00
9 changed files with 153 additions and 11 deletions

View File

@@ -1,2 +1,3 @@
example
.travis.yml
.git

View File

@@ -1,4 +1,4 @@
FROM golang:1.12-alpine as builder
FROM golang:1.13-alpine as builder
# Setup
RUN mkdir -p /go/src/github.com/thomseddon/traefik-forward-auth

View File

@@ -1,4 +1,4 @@
FROM golang:1.12-alpine as builder
FROM golang:1.13-alpine as builder
# Setup
RUN mkdir -p /go/src/github.com/thomseddon/traefik-forward-auth

View File

@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
@@ -81,9 +82,35 @@ func ValidateEmail(email string) bool {
return found
}
// ValidateRedirect validates that the given redirect is valid and permitted for
// the given request
func ValidateRedirect(r *http.Request, redirect string) error {
redirectURL, err := url.Parse(redirect)
if err != nil {
return errors.New("Unable to parse redirect")
}
// If we're using an auth domain?
if use, base := useAuthDomain(r); use {
// If we are using an auth domain, they redirect must share a common
// suffix with the requested redirect
if !strings.HasSuffix(redirectURL.Host, base) {
return errors.New("Redirect host does not match any expected hosts (should match cookie domain when using auth host)")
}
} else {
// If not, we should only ever redirect to the same domain
if redirectURL.Host != r.Header.Get("X-Forwarded-Host") {
return errors.New("Redirect host does not match request host (must match when not using auth host)")
}
}
return nil
}
// Utility methods
// Get the redirect base
// Get the request base from forwarded request
func redirectBase(r *http.Request) string {
proto := r.Header.Get("X-Forwarded-Proto")
host := r.Header.Get("X-Forwarded-Host")

View File

@@ -95,6 +95,96 @@ func TestAuthValidateEmail(t *testing.T) {
assert.True(v, "should allow user in whitelist")
}
func TestAuthValidateRedirect(t *testing.T) {
assert := assert.New(t)
config, _ = NewConfig([]string{})
newRedirectRequest := func(urlStr string) *http.Request {
u, err := url.Parse(urlStr)
assert.Nil(err)
r, _ := http.NewRequest("GET", urlStr, nil)
r.Header.Add("X-Forwarded-Proto", u.Scheme)
r.Header.Add("X-Forwarded-Host", u.Host)
r.Header.Add("X-Forwarded-Uri", u.RequestURI())
return r
}
errStr := "Redirect host does not match request host (must match when not using auth host)"
err := ValidateRedirect(
newRedirectRequest("http://app.example.com/_oauth?state=123"),
"http://app.example.com.bad.com",
)
if assert.Error(err) {
assert.Equal(errStr, err.Error(), "Should not allow redirect to subdomain")
}
err = ValidateRedirect(
newRedirectRequest("http://app.example.com/_oauth?state=123"),
"http://app.example.combad.com",
)
if assert.Error(err) {
assert.Equal(errStr, err.Error(), "Should not allow redirect to overlapping domain")
}
err = ValidateRedirect(
newRedirectRequest("http://app.example.com/_oauth?state=123"),
"http://example.com",
)
if assert.Error(err) {
assert.Equal(errStr, err.Error(), "Should not allow redirect from subdomain")
}
err = ValidateRedirect(
newRedirectRequest("http://app.example.com/_oauth?state=123"),
"http://app.example.com/profile",
)
assert.Nil(err, "Should allow same domain")
//
// With Auth Host
//
config.AuthHost = "auth.example.com"
config.CookieDomains = []CookieDomain{*NewCookieDomain("example.com")}
errStr = "Redirect host does not match any expected hosts (should match cookie domain when using auth host)"
err = ValidateRedirect(
newRedirectRequest("http://app.example.com/_oauth?state=123"),
"http://app.example.com.bad.com",
)
if assert.Error(err) {
assert.Equal(errStr, err.Error(), "Should not allow redirect to subdomain")
}
err = ValidateRedirect(
newRedirectRequest("http://app.example.com/_oauth?state=123"),
"http://app.example.combad.com",
)
if assert.Error(err) {
assert.Equal(errStr, err.Error(), "Should not allow redirect to overlapping domain")
}
err = ValidateRedirect(
newRedirectRequest("http://auth.example.com/_oauth?state=123"),
"http://app.example.com/profile",
)
assert.Nil(err, "Should allow between subdomains when using auth host")
err = ValidateRedirect(
newRedirectRequest("http://auth.example.com/_oauth?state=123"),
"http://auth.example.com/profile",
)
assert.Nil(err, "Should allow same domain when using auth host")
err = ValidateRedirect(
newRedirectRequest("http://auth.example.com/_oauth?state=123"),
"http://example.com/profile",
)
assert.Nil(err, "Should allow from subdomain when using auth host")
}
func TestRedirectUri(t *testing.T) {
assert := assert.New(t)

View File

@@ -25,17 +25,17 @@ type Config struct {
AuthHost string `long:"auth-host" env:"AUTH_HOST" description:"Single host to use when returning from 3rd party auth"`
Config func(s string) error `long:"config" env:"CONFIG" description:"Path to config file" json:"-"`
CookieDomains []CookieDomain `long:"cookie-domain" env:"COOKIE_DOMAIN" description:"Domain to set auth cookie on, can be set multiple times"`
CookieDomains []CookieDomain `long:"cookie-domain" env:"COOKIE_DOMAIN" env-delim:"," description:"Domain to set auth cookie on, can be set multiple times"`
InsecureCookie bool `long:"insecure-cookie" env:"INSECURE_COOKIE" description:"Use insecure cookies"`
CookieName string `long:"cookie-name" env:"COOKIE_NAME" default:"_forward_auth" description:"Cookie Name"`
CSRFCookieName string `long:"csrf-cookie-name" env:"CSRF_COOKIE_NAME" default:"_forward_auth_csrf" description:"CSRF Cookie Name"`
DefaultAction string `long:"default-action" env:"DEFAULT_ACTION" default:"auth" choice:"auth" choice:"allow" description:"Default action"`
DefaultProvider string `long:"default-provider" env:"DEFAULT_PROVIDER" default:"google" choice:"google" choice:"oidc" description:"Default provider"`
Domains CommaSeparatedList `long:"domain" env:"DOMAIN" description:"Only allow given email domains, can be set multiple times"`
Domains CommaSeparatedList `long:"domain" env:"DOMAIN" env-delim:"," description:"Only allow given email domains, can be set multiple times"`
LifetimeString int `long:"lifetime" env:"LIFETIME" default:"43200" description:"Lifetime in seconds"`
Path string `long:"url-path" env:"URL_PATH" default:"/_oauth" description:"Callback URL Path"`
SecretString string `long:"secret" env:"SECRET" description:"Secret used for signing (required)" json:"-"`
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" description:"Only allow given email addresses, can be set multiple times"`
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" env-delim:"," description:"Only allow given email addresses, can be set multiple times"`
Providers provider.Providers `group:"providers" namespace:"providers" env-namespace:"PROVIDERS"`
Rules map[string]*Rule `long:"rule.<name>.<param>" description:"Rule definitions, param can be: \"action\", \"rule\" or \"provider\""`

View File

@@ -197,14 +197,27 @@ func TestConfigParseEnvironment(t *testing.T) {
assert := assert.New(t)
os.Setenv("COOKIE_NAME", "env_cookie_name")
os.Setenv("PROVIDERS_GOOGLE_CLIENT_ID", "env_client_id")
os.Setenv("COOKIE_DOMAIN", "test1.com,example.org")
os.Setenv("DOMAIN", "test2.com,example.org")
os.Setenv("WHITELIST", "test3.com,example.org")
c, err := NewConfig([]string{})
assert.Nil(err)
assert.Equal("env_cookie_name", c.CookieName, "variable should be read from environment")
assert.Equal("env_client_id", c.Providers.Google.ClientID, "namespace variable should be read from environment")
assert.Equal([]CookieDomain{
*NewCookieDomain("test1.com"),
*NewCookieDomain("example.org"),
}, c.CookieDomains, "array variable should be read from environment COOKIE_DOMAIN")
assert.Equal(CommaSeparatedList{"test2.com", "example.org"}, c.Domains, "array variable should be read from environment DOMAIN")
assert.Equal(CommaSeparatedList{"test3.com", "example.org"}, c.Whitelist, "array variable should be read from environment WHITELIST")
os.Unsetenv("COOKIE_NAME")
os.Unsetenv("PROVIDERS_GOOGLE_CLIENT_ID")
os.Unsetenv("COOKIE_DOMAIN")
os.Unsetenv("DOMAIN")
os.Unsetenv("WHITELIST")
}
func TestConfigParseEnvironmentBackwardsCompatability(t *testing.T) {

View File

@@ -143,6 +143,16 @@ func (s *Server) AuthCallbackHandler() http.HandlerFunc {
// Clear CSRF cookie
http.SetCookie(w, ClearCSRFCookie(r))
// Validate redirect
err = ValidateRedirect(r, redirect)
if err != nil {
logger.WithFields(logrus.Fields{
"receieved_redirect": redirect,
}).Warnf("Invalid redirect in CSRF. %v", err)
http.Error(w, "Not authorized", 401)
return
}
// Exchange code for token
token, err := p.ExchangeCode(redirectUri(r), r.URL.Query().Get("code"))
if err != nil {
@@ -193,7 +203,8 @@ func (s *Server) authRedirect(logger *logrus.Entry, w http.ResponseWriter, r *ht
func (s *Server) logger(r *http.Request, rule, msg string) *logrus.Entry {
// Create logger
logger := log.WithFields(logrus.Fields{
"source_ip": r.Header.Get("X-Forwarded-For"),
"source_ip": r.Header.Get("X-Forwarded-For"),
"request_host": r.Header.Get("X-Forwarded-Host"),
})
// Log request

View File

@@ -141,20 +141,20 @@ func TestServerAuthCallback(t *testing.T) {
assert.Equal(401, res.StatusCode, "auth callback without cookie shouldn't be authorised")
// Should catch invalid csrf cookie
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:http://redirect")
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:http://example.com")
c := MakeCSRFCookie(req, "nononononononononononononononono")
res, _ = doHttpRequest(req, c)
assert.Equal(401, res.StatusCode, "auth callback with invalid cookie shouldn't be authorised")
// Should redirect valid request
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:google:http://redirect")
req = newDefaultHttpRequest("/_oauth?state=12345678901234567890123456789012:google:http://example.com")
c = MakeCSRFCookie(req, "12345678901234567890123456789012")
res, _ = doHttpRequest(req, c)
assert.Equal(307, res.StatusCode, "valid auth callback should be allowed")
require.Equal(t, 307, res.StatusCode, "valid auth callback should be allowed")
fwd, _ := res.Location()
assert.Equal("http", fwd.Scheme, "valid request should be redirected to return url")
assert.Equal("redirect", fwd.Host, "valid request should be redirected to return url")
assert.Equal("example.com", fwd.Host, "valid request should be redirected to return url")
assert.Equal("", fwd.Path, "valid request should be redirected to return url")
}