mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2026-10-17 20:17:03 +02:00
Support HTTP method based allowlists
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/pkg/ip"
|
||||
)
|
||||
|
||||
func validateAllowlists(o *options.Options) []string {
|
||||
msgs := []string{}
|
||||
|
||||
msgs = append(msgs, validateRoutes(o)...)
|
||||
msgs = append(msgs, validateRegexes(o)...)
|
||||
msgs = append(msgs, validateTrustedIPs(o)...)
|
||||
|
||||
if len(o.TrustedIPs) > 0 && o.ReverseProxy {
|
||||
_, err := fmt.Fprintln(os.Stderr, "WARNING: mixing --trusted-ip with --reverse-proxy is a potential security vulnerability. An attacker can inject a trusted IP into an X-Real-IP or X-Forwarded-For header if they aren't properly protected outside of oauth2-proxy")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return msgs
|
||||
}
|
||||
|
||||
// validateRoutes validates method=path routes passed with options.SkipAuthRoutes
|
||||
func validateRoutes(o *options.Options) []string {
|
||||
msgs := []string{}
|
||||
for _, route := range o.SkipAuthRoutes {
|
||||
var regex string
|
||||
parts := strings.Split(route, "=")
|
||||
if len(parts) == 1 {
|
||||
regex = parts[0]
|
||||
} else {
|
||||
regex = strings.Join(parts[1:], "=")
|
||||
}
|
||||
_, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
msgs = append(msgs, fmt.Sprintf("error compiling regex /%s/: %v", regex, err))
|
||||
}
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
// validateRegex validates regex paths passed with options.SkipAuthRegex
|
||||
func validateRegexes(o *options.Options) []string {
|
||||
msgs := []string{}
|
||||
for _, regex := range o.SkipAuthRegex {
|
||||
_, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
msgs = append(msgs, fmt.Sprintf("error compiling regex /%s/: %v", regex, err))
|
||||
}
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
// validateTrustedIPs validates IP/CIDRs for IP based allowlists
|
||||
func validateTrustedIPs(o *options.Options) []string {
|
||||
msgs := []string{}
|
||||
for i, ipStr := range o.TrustedIPs {
|
||||
if nil == ip.ParseIPNet(ipStr) {
|
||||
msgs = append(msgs, fmt.Sprintf("trusted_ips[%d] (%s) could not be recognized", i, ipStr))
|
||||
}
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_validateAllowlists(t *testing.T) {
|
||||
opts := &options.Options{
|
||||
SkipAuthRoutes: []string{
|
||||
"POST=/foo/bar",
|
||||
"PUT=^/foo/bar$",
|
||||
},
|
||||
SkipAuthRegex: []string{"/foo/baz"},
|
||||
TrustedIPs: []string{
|
||||
"10.32.0.1/32",
|
||||
"43.36.201.0/24",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, []string{}, validateAllowlists(opts))
|
||||
}
|
||||
|
||||
func Test_validateRoutes(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
Regexes []string
|
||||
Expected []string
|
||||
}{
|
||||
"Valid regex routes": {
|
||||
Regexes: []string{
|
||||
"/foo",
|
||||
"POST=/foo/bar",
|
||||
"PUT=^/foo/bar$",
|
||||
"DELETE=/crazy/(?:regex)?/[^/]+/stuff$",
|
||||
},
|
||||
Expected: []string{},
|
||||
},
|
||||
"Bad regexes do not compile": {
|
||||
Regexes: []string{
|
||||
"POST=/(foo",
|
||||
"OPTIONS=/foo/bar)",
|
||||
"GET=^]/foo/bar[$",
|
||||
"GET=^]/foo/bar[$",
|
||||
},
|
||||
Expected: []string{
|
||||
"error compiling regex //(foo/: error parsing regexp: missing closing ): `/(foo`",
|
||||
"error compiling regex //foo/bar)/: error parsing regexp: unexpected ): `/foo/bar)`",
|
||||
"error compiling regex /^]/foo/bar[$/: error parsing regexp: missing closing ]: `[$`",
|
||||
"error compiling regex /^]/foo/bar[$/: error parsing regexp: missing closing ]: `[$`",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for testName, tc := range testCases {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
opts := &options.Options{
|
||||
SkipAuthRoutes: tc.Regexes,
|
||||
}
|
||||
msgs := validateRoutes(opts)
|
||||
assert.Equal(t, tc.Expected, msgs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_validateRegexes(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
Regexes []string
|
||||
Expected []string
|
||||
}{
|
||||
"Valid regex routes": {
|
||||
Regexes: []string{
|
||||
"/foo",
|
||||
"/foo/bar",
|
||||
"^/foo/bar$",
|
||||
"/crazy/(?:regex)?/[^/]+/stuff$",
|
||||
},
|
||||
Expected: []string{},
|
||||
},
|
||||
"Bad regexes do not compile": {
|
||||
Regexes: []string{
|
||||
"/(foo",
|
||||
"/foo/bar)",
|
||||
"^]/foo/bar[$",
|
||||
"^]/foo/bar[$",
|
||||
},
|
||||
Expected: []string{
|
||||
"error compiling regex //(foo/: error parsing regexp: missing closing ): `/(foo`",
|
||||
"error compiling regex //foo/bar)/: error parsing regexp: unexpected ): `/foo/bar)`",
|
||||
"error compiling regex /^]/foo/bar[$/: error parsing regexp: missing closing ]: `[$`",
|
||||
"error compiling regex /^]/foo/bar[$/: error parsing regexp: missing closing ]: `[$`",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for testName, tc := range testCases {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
opts := &options.Options{
|
||||
SkipAuthRegex: tc.Regexes,
|
||||
}
|
||||
msgs := validateRegexes(opts)
|
||||
assert.Equal(t, tc.Expected, msgs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_validateTrustedIPs(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
TrustedIPs []string
|
||||
Expected []string
|
||||
}{
|
||||
"Non-overlapping valid IPs": {
|
||||
TrustedIPs: []string{
|
||||
"127.0.0.1",
|
||||
"10.32.0.1/32",
|
||||
"43.36.201.0/24",
|
||||
"::1",
|
||||
"2a12:105:ee7:9234:0:0:0:0/64",
|
||||
},
|
||||
Expected: []string{},
|
||||
},
|
||||
"Overlapping valid IPs": {
|
||||
TrustedIPs: []string{
|
||||
"135.180.78.199",
|
||||
"135.180.78.199/32",
|
||||
"d910:a5a1:16f8:ddf5:e5b9:5cef:a65e:41f4",
|
||||
"d910:a5a1:16f8:ddf5:e5b9:5cef:a65e:41f4/128",
|
||||
},
|
||||
Expected: []string{},
|
||||
},
|
||||
"Invalid IPs": {
|
||||
TrustedIPs: []string{"[::1]", "alkwlkbn/32"},
|
||||
Expected: []string{
|
||||
"trusted_ips[0] ([::1]) could not be recognized",
|
||||
"trusted_ips[1] (alkwlkbn/32) could not be recognized",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for testName, tc := range testCases {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
opts := &options.Options{
|
||||
TrustedIPs: tc.TrustedIPs,
|
||||
}
|
||||
msgs := validateTrustedIPs(opts)
|
||||
assert.Equal(t, tc.Expected, msgs)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
@@ -184,15 +183,6 @@ func Validate(o *options.Options) error {
|
||||
o.SetRedirectURL(redirectURL)
|
||||
|
||||
msgs = append(msgs, validateUpstreams(o.UpstreamServers)...)
|
||||
|
||||
for _, u := range o.SkipAuthRegex {
|
||||
compiledRegex, err := regexp.Compile(u)
|
||||
if err != nil {
|
||||
msgs = append(msgs, fmt.Sprintf("error compiling regex=%q %s", u, err))
|
||||
continue
|
||||
}
|
||||
o.SetCompiledRegex(append(o.GetCompiledRegex(), compiledRegex))
|
||||
}
|
||||
msgs = parseProviderInfo(o, msgs)
|
||||
|
||||
if len(o.GoogleGroups) > 0 || o.GoogleAdminEmail != "" || o.GoogleServiceAccountJSON != "" {
|
||||
@@ -223,18 +213,8 @@ func Validate(o *options.Options) error {
|
||||
})
|
||||
}
|
||||
|
||||
if len(o.TrustedIPs) > 0 && o.ReverseProxy {
|
||||
_, err := fmt.Fprintln(os.Stderr, "WARNING: trusting of IPs with --reverse-proxy poses risks if a header spoofing attack is possible.")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
for i, ipStr := range o.TrustedIPs {
|
||||
if nil == ip.ParseIPNet(ipStr) {
|
||||
msgs = append(msgs, fmt.Sprintf("trusted_ips[%d] (%s) could not be recognized", i, ipStr))
|
||||
}
|
||||
}
|
||||
// Do this after ReverseProxy validation for TrustedIP coordinated checks
|
||||
msgs = append(msgs, validateAllowlists(o)...)
|
||||
|
||||
if len(msgs) != 0 {
|
||||
return fmt.Errorf("invalid configuration:\n %s",
|
||||
|
||||
@@ -2,7 +2,6 @@ package validation
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -78,12 +77,19 @@ func TestClientSecretFileOption(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
f.WriteString("testcase")
|
||||
_, err = f.WriteString("testcase")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write to temp file: %v", err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
t.Fatalf("failed to close temp file: %v", err)
|
||||
}
|
||||
clientSecretFileName := f.Name()
|
||||
defer os.Remove(clientSecretFileName)
|
||||
defer func(t *testing.T) {
|
||||
if err := os.Remove(clientSecretFileName); err != nil {
|
||||
t.Fatalf("failed to delete temp file: %v", err)
|
||||
}
|
||||
}(t)
|
||||
|
||||
o := options.NewOptions()
|
||||
o.Cookie.Secret = cookieSecret
|
||||
@@ -144,41 +150,6 @@ func TestRedirectURL(t *testing.T) {
|
||||
assert.Equal(t, expected, o.GetRedirectURL())
|
||||
}
|
||||
|
||||
func TestCompiledRegex(t *testing.T) {
|
||||
o := testOptions()
|
||||
regexps := []string{"/foo/.*", "/ba[rz]/quux"}
|
||||
o.SkipAuthRegex = regexps
|
||||
assert.Equal(t, nil, Validate(o))
|
||||
actual := make([]string, 0)
|
||||
for _, regex := range o.GetCompiledRegex() {
|
||||
actual = append(actual, regex.String())
|
||||
}
|
||||
assert.Equal(t, regexps, actual)
|
||||
}
|
||||
|
||||
func TestCompiledRegexError(t *testing.T) {
|
||||
o := testOptions()
|
||||
o.SkipAuthRegex = []string{"(foobaz", "barquux)"}
|
||||
err := Validate(o)
|
||||
assert.NotEqual(t, nil, err)
|
||||
|
||||
expected := errorMsg([]string{
|
||||
"error compiling regex=\"(foobaz\" error parsing regexp: " +
|
||||
"missing closing ): `(foobaz`",
|
||||
"error compiling regex=\"barquux)\" error parsing regexp: " +
|
||||
"unexpected ): `barquux)`"})
|
||||
assert.Equal(t, expected, err.Error())
|
||||
|
||||
o.SkipAuthRegex = []string{"foobaz", "barquux)"}
|
||||
err = Validate(o)
|
||||
assert.NotEqual(t, nil, err)
|
||||
|
||||
expected = errorMsg([]string{
|
||||
"error compiling regex=\"barquux)\" error parsing regexp: " +
|
||||
"unexpected ): `barquux)`"})
|
||||
assert.Equal(t, expected, err.Error())
|
||||
}
|
||||
|
||||
func TestDefaultProviderApiSettings(t *testing.T) {
|
||||
o := testOptions()
|
||||
assert.Equal(t, nil, Validate(o))
|
||||
@@ -337,45 +308,6 @@ func TestRealClientIPHeader(t *testing.T) {
|
||||
assert.Nil(t, o.GetRealClientIPParser())
|
||||
}
|
||||
|
||||
func TestIPCIDRSetOption(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
trustedIPs []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
"TestSomeIPs",
|
||||
[]string{"127.0.0.1", "10.32.0.1/32", "43.36.201.0/24", "::1", "2a12:105:ee7:9234:0:0:0:0/64"},
|
||||
nil,
|
||||
}, {
|
||||
"TestOverlappingIPs",
|
||||
[]string{"135.180.78.199", "135.180.78.199/32", "d910:a5a1:16f8:ddf5:e5b9:5cef:a65e:41f4", "d910:a5a1:16f8:ddf5:e5b9:5cef:a65e:41f4/128"},
|
||||
nil,
|
||||
}, {
|
||||
"TestInvalidIPs",
|
||||
[]string{"[::1]", "alkwlkbn/32"},
|
||||
errors.New(
|
||||
"invalid configuration:\n" +
|
||||
" trusted_ips[0] ([::1]) could not be recognized\n" +
|
||||
" trusted_ips[1] (alkwlkbn/32) could not be recognized",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := testOptions()
|
||||
o.TrustedIPs = tt.trustedIPs
|
||||
err := Validate(o)
|
||||
if tt.err == nil {
|
||||
assert.Nil(t, err)
|
||||
} else {
|
||||
assert.Equal(t, tt.err.Error(), err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderCAFilesError(t *testing.T) {
|
||||
file, err := ioutil.TempFile("", "absent.*.crt")
|
||||
assert.NoError(t, err)
|
||||
|
||||
Reference in New Issue
Block a user