From cce0eaa974599d7fc2de2698c0a2b7d1a3ec730f Mon Sep 17 00:00:00 2001 From: Scott McKendry Date: Tue, 28 Jul 2026 19:49:00 +1200 Subject: [PATCH] rework to keep regex under specific conditions --- internal/controller/proxy_controller.go | 12 ++++-- internal/service/access_controls_rules.go | 40 ++++++++++++++----- .../service/access_controls_rules_test.go | 34 ++++++---------- 3 files changed, 51 insertions(+), 35 deletions(-) diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index 3637a331..01326c61 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -9,14 +9,14 @@ import ( "regexp" "strings" + "github.com/gin-gonic/gin" + "github.com/google/go-querystring/query" + "go.uber.org/dig" + "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils/logger" - "go.uber.org/dig" - - "github.com/gin-gonic/gin" - "github.com/google/go-querystring/query" ) type AuthModuleType int @@ -396,6 +396,10 @@ func (controller *ProxyController) getForwardAuthContext(c *gin.Context) (ProxyC return ProxyContext{}, fmt.Errorf("invalid x-forwarded-uri: %w", err) } + if parsedURI.Path == "" { + parsedURI.Path = "/" + } + proto, ok := controller.getHeader(c, "x-forwarded-proto") if !ok { diff --git a/internal/service/access_controls_rules.go b/internal/service/access_controls_rules.go index be6dc8b8..3062fe82 100644 --- a/internal/service/access_controls_rules.go +++ b/internal/service/access_controls_rules.go @@ -2,6 +2,8 @@ package service import ( "errors" + "fmt" + "regexp" "strings" "github.com/tinyauthapp/tinyauth/internal/model" @@ -179,26 +181,38 @@ type AuthEnabledRule struct { Log *logger.Logger } -func matchPathRule(paths, path string) bool { +func matchPathRule(paths, path string) (bool, error) { for _, configuredPath := range strings.Split(paths, ",") { configuredPath = strings.TrimSpace(configuredPath) if configuredPath == "/" { - return true + return true, nil } - configuredPath = strings.TrimRight(configuredPath, "/") - if configuredPath == "" { continue } - if path == configuredPath || strings.HasPrefix(path, configuredPath+"/") { - return true + // only apply regex if the path starts and ends with a slash, e.g. /regex/ + if strings.HasPrefix(configuredPath, "/") && strings.HasSuffix(configuredPath, "/") { + regex, err := regexp.Compile(configuredPath[1 : len(configuredPath)-1]) + if err != nil { + return false, fmt.Errorf("invalid path regex %q: %w", configuredPath, err) + } + + if regex.MatchString(path) { + return true, nil + } + + continue + } + + if strings.HasPrefix(path, configuredPath) { + return true, nil } } - return false + return false, nil } func (rule *AuthEnabledRule) Evaluate(ctx *ACLContext) Effect { @@ -207,7 +221,11 @@ func (rule *AuthEnabledRule) Evaluate(ctx *ACLContext) Effect { } if ctx.ACLs.Path.Block != "" { - match := matchPathRule(ctx.ACLs.Path.Block, ctx.Path) + match, err := matchPathRule(ctx.ACLs.Path.Block, ctx.Path) + if err != nil { + rule.Log.App.Warn().Err(err).Msg("Invalid path block rule") + return EffectDeny + } if !match { return EffectAllow @@ -215,7 +233,11 @@ func (rule *AuthEnabledRule) Evaluate(ctx *ACLContext) Effect { } if ctx.ACLs.Path.Allow != "" { - match := matchPathRule(ctx.ACLs.Path.Allow, ctx.Path) + match, err := matchPathRule(ctx.ACLs.Path.Allow, ctx.Path) + if err != nil { + rule.Log.App.Warn().Err(err).Msg("Invalid path allow rule") + return EffectDeny + } if match { return EffectAllow diff --git a/internal/service/access_controls_rules_test.go b/internal/service/access_controls_rules_test.go index 39009b71..381206a3 100644 --- a/internal/service/access_controls_rules_test.go +++ b/internal/service/access_controls_rules_test.go @@ -527,32 +527,22 @@ func TestAuthEnabledRule(t *testing.T) { expected: EffectDeny, }, { - name: "allows when path does not match block path", - ctx: &ACLContext{ - ACLs: &model.App{ - Path: model.AppPath{Block: "/admin"}, - }, - Path: "/public", - }, - expected: EffectAllow, - }, - { - name: "denies when path matches block path", - ctx: &ACLContext{ - ACLs: &model.App{ - Path: model.AppPath{Block: "/admin"}, - }, - Path: "/admin/users", - }, - expected: EffectDeny, - }, - { - name: "allows when path matches allow path", + name: "allows when path starts with allow path", ctx: &ACLContext{ ACLs: &model.App{ Path: model.AppPath{Allow: "/public"}, }, - Path: "/public/index", + Path: "/publicity", + }, + expected: EffectAllow, + }, + { + name: "allows when path matches allow regex", + ctx: &ACLContext{ + ACLs: &model.App{ + Path: model.AppPath{Allow: "/^/public-[0-9]+$/"}, + }, + Path: "/public-42", }, expected: EffectAllow, },