mirror of
https://github.com/tinyauthapp/tinyauth.git
synced 2026-08-11 19:43:43 +08:00
fix(acls): auth bypass via forward auth
This commit is contained in:
@@ -390,6 +390,12 @@ func (controller *ProxyController) getForwardAuthContext(c *gin.Context) (ProxyC
|
|||||||
return ProxyContext{}, errors.New("x-forwarded-uri not found")
|
return ProxyContext{}, errors.New("x-forwarded-uri not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parsedURI, err := url.ParseRequestURI(uri)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ProxyContext{}, fmt.Errorf("invalid x-forwarded-uri: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
proto, ok := controller.getHeader(c, "x-forwarded-proto")
|
proto, ok := controller.getHeader(c, "x-forwarded-proto")
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -403,7 +409,7 @@ func (controller *ProxyController) getForwardAuthContext(c *gin.Context) (ProxyC
|
|||||||
return ProxyContext{
|
return ProxyContext{
|
||||||
Host: host,
|
Host: host,
|
||||||
Proto: proto,
|
Proto: proto,
|
||||||
Path: uri,
|
Path: parsedURI.Path,
|
||||||
Method: method,
|
Method: method,
|
||||||
Type: ForwardAuth,
|
Type: ForwardAuth,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -287,6 +287,66 @@ func TestProxyController(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Ensure path allow ACL does not match forwarded URI query string",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "path-allow.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/admin?path=/allowed")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Ensure path allow ACL does not match path substrings",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "path-allow.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/admin/allowed")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Ensure path block ACL works on forward auth",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "path-block.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/blocked")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Ensure path block ACL does not match forwarded URI query string",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "path-block.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/admin?path=/blocked")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Ensure path block ACL does not match path substrings",
|
||||||
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
|
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
|
req.Header.Set("x-forwarded-host", "path-block.example.com")
|
||||||
|
req.Header.Set("x-forwarded-proto", "https")
|
||||||
|
req.Header.Set("x-forwarded-uri", "/admin/blocked")
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "Ensure path allow ACL works on nginx auth request",
|
description: "Ensure path allow ACL works on nginx auth request",
|
||||||
middlewares: []gin.HandlerFunc{},
|
middlewares: []gin.HandlerFunc{},
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
@@ -180,33 +179,45 @@ type AuthEnabledRule struct {
|
|||||||
Log *logger.Logger
|
Log *logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func matchPathRule(paths, path string) bool {
|
||||||
|
for _, configuredPath := range strings.Split(paths, ",") {
|
||||||
|
configuredPath = strings.TrimSpace(configuredPath)
|
||||||
|
|
||||||
|
if configuredPath == "/" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
configuredPath = strings.TrimRight(configuredPath, "/")
|
||||||
|
|
||||||
|
if configuredPath == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == configuredPath || strings.HasPrefix(path, configuredPath+"/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (rule *AuthEnabledRule) Evaluate(ctx *ACLContext) Effect {
|
func (rule *AuthEnabledRule) Evaluate(ctx *ACLContext) Effect {
|
||||||
if ctx.ACLs == nil {
|
if ctx.ACLs == nil {
|
||||||
return EffectDeny
|
return EffectDeny
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.ACLs.Path.Block != "" {
|
if ctx.ACLs.Path.Block != "" {
|
||||||
regex, err := regexp.Compile(ctx.ACLs.Path.Block)
|
match := matchPathRule(ctx.ACLs.Path.Block, ctx.Path)
|
||||||
|
|
||||||
if err != nil {
|
if !match {
|
||||||
rule.Log.App.Error().Err(err).Msg("Failed to compile block regex")
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
|
|
||||||
if !regex.MatchString(ctx.Path) {
|
|
||||||
return EffectAllow
|
return EffectAllow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.ACLs.Path.Allow != "" {
|
if ctx.ACLs.Path.Allow != "" {
|
||||||
regex, err := regexp.Compile(ctx.ACLs.Path.Allow)
|
match := matchPathRule(ctx.ACLs.Path.Allow, ctx.Path)
|
||||||
|
|
||||||
if err != nil {
|
if match {
|
||||||
rule.Log.App.Error().Err(err).Msg("Failed to compile allow regex")
|
|
||||||
return EffectDeny
|
|
||||||
}
|
|
||||||
|
|
||||||
if regex.MatchString(ctx.Path) {
|
|
||||||
return EffectAllow
|
return EffectAllow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -527,52 +527,52 @@ func TestAuthEnabledRule(t *testing.T) {
|
|||||||
expected: EffectDeny,
|
expected: EffectDeny,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "allows when path does not match block regex",
|
name: "allows when path does not match block path",
|
||||||
ctx: &ACLContext{
|
ctx: &ACLContext{
|
||||||
ACLs: &model.App{
|
ACLs: &model.App{
|
||||||
Path: model.AppPath{Block: "^/admin"},
|
Path: model.AppPath{Block: "/admin"},
|
||||||
},
|
},
|
||||||
Path: "/public",
|
Path: "/public",
|
||||||
},
|
},
|
||||||
expected: EffectAllow,
|
expected: EffectAllow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "denies when path matches block regex and no allow regex",
|
name: "denies when path matches block path",
|
||||||
ctx: &ACLContext{
|
ctx: &ACLContext{
|
||||||
ACLs: &model.App{
|
ACLs: &model.App{
|
||||||
Path: model.AppPath{Block: "^/admin"},
|
Path: model.AppPath{Block: "/admin"},
|
||||||
},
|
},
|
||||||
Path: "/admin/users",
|
Path: "/admin/users",
|
||||||
},
|
},
|
||||||
expected: EffectDeny,
|
expected: EffectDeny,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "allows when path matches allow regex",
|
name: "allows when path matches allow path",
|
||||||
ctx: &ACLContext{
|
ctx: &ACLContext{
|
||||||
ACLs: &model.App{
|
ACLs: &model.App{
|
||||||
Path: model.AppPath{Allow: "^/public"},
|
Path: model.AppPath{Allow: "/public"},
|
||||||
},
|
},
|
||||||
Path: "/public/index",
|
Path: "/public/index",
|
||||||
},
|
},
|
||||||
expected: EffectAllow,
|
expected: EffectAllow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "denies when path does not match allow regex",
|
name: "denies when path does not match allow path",
|
||||||
ctx: &ACLContext{
|
ctx: &ACLContext{
|
||||||
ACLs: &model.App{
|
ACLs: &model.App{
|
||||||
Path: model.AppPath{Allow: "^/public"},
|
Path: model.AppPath{Allow: "/public"},
|
||||||
},
|
},
|
||||||
Path: "/private",
|
Path: "/private",
|
||||||
},
|
},
|
||||||
expected: EffectDeny,
|
expected: EffectDeny,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "allows when blocked path is also explicitly allowed",
|
name: "allows when blocked path is explicitly allowed",
|
||||||
ctx: &ACLContext{
|
ctx: &ACLContext{
|
||||||
ACLs: &model.App{
|
ACLs: &model.App{
|
||||||
Path: model.AppPath{
|
Path: model.AppPath{
|
||||||
Block: "^/admin",
|
Block: "/admin",
|
||||||
Allow: "^/admin/public",
|
Allow: "/admin/public",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Path: "/admin/public/page",
|
Path: "/admin/public/page",
|
||||||
@@ -580,20 +580,10 @@ func TestAuthEnabledRule(t *testing.T) {
|
|||||||
expected: EffectAllow,
|
expected: EffectAllow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "denies when block regex fails to compile",
|
name: "denies when root is blocked",
|
||||||
ctx: &ACLContext{
|
ctx: &ACLContext{
|
||||||
ACLs: &model.App{
|
ACLs: &model.App{
|
||||||
Path: model.AppPath{Block: "[invalid"},
|
Path: model.AppPath{Block: "/"},
|
||||||
},
|
|
||||||
Path: "/anything",
|
|
||||||
},
|
|
||||||
expected: EffectDeny,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "denies when allow regex fails to compile",
|
|
||||||
ctx: &ACLContext{
|
|
||||||
ACLs: &model.App{
|
|
||||||
Path: model.AppPath{Allow: "[invalid"},
|
|
||||||
},
|
},
|
||||||
Path: "/anything",
|
Path: "/anything",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,6 +61,14 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
|
|||||||
Allow: "/allowed",
|
Allow: "/allowed",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"app_path_block": {
|
||||||
|
Config: model.AppConfig{
|
||||||
|
Domain: "path-block.example.com",
|
||||||
|
},
|
||||||
|
Path: model.AppPath{
|
||||||
|
Block: "/blocked",
|
||||||
|
},
|
||||||
|
},
|
||||||
"app_user_allow": {
|
"app_user_allow": {
|
||||||
Config: model.AppConfig{
|
Config: model.AppConfig{
|
||||||
Domain: "user-allow.example.com",
|
Domain: "user-allow.example.com",
|
||||||
|
|||||||
Reference in New Issue
Block a user