mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-06-10 14:28:15 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8acbf6505 | |||
| 2c1237b5c0 | |||
| 9f479f578a | |||
| c184028d42 | |||
| 0491f1fac2 | |||
| d3a00da448 |
@@ -39,3 +39,5 @@ wenet
|
|||||||
qwertiko
|
qwertiko
|
||||||
setuplistener
|
setuplistener
|
||||||
mba
|
mba
|
||||||
|
xfu
|
||||||
|
xou
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ fahedouch
|
|||||||
fastcgi
|
fastcgi
|
||||||
FCr
|
FCr
|
||||||
fcrdns
|
fcrdns
|
||||||
|
fcvg
|
||||||
fediverse
|
fediverse
|
||||||
ffprobe
|
ffprobe
|
||||||
fhdr
|
fhdr
|
||||||
@@ -238,6 +239,7 @@ mnt
|
|||||||
Mojeek
|
Mojeek
|
||||||
mojeekbot
|
mojeekbot
|
||||||
mozilla
|
mozilla
|
||||||
|
mqvh
|
||||||
myclient
|
myclient
|
||||||
mymaster
|
mymaster
|
||||||
mypass
|
mypass
|
||||||
@@ -387,6 +389,7 @@ vnd
|
|||||||
VPS
|
VPS
|
||||||
Vultr
|
Vultr
|
||||||
WAIFU
|
WAIFU
|
||||||
|
wcg
|
||||||
weblate
|
weblate
|
||||||
webmaster
|
webmaster
|
||||||
webpage
|
webpage
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ jobs:
|
|||||||
- palemoon/amd64
|
- palemoon/amd64
|
||||||
#- palemoon/i386
|
#- palemoon/i386
|
||||||
- robots_txt
|
- robots_txt
|
||||||
|
- traefik
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
+1
-1
@@ -259,7 +259,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lg.Info("loading policy file", "fname", *policyFname)
|
lg.Info("loading policy file", "fname", *policyFname)
|
||||||
policy, err := libanubis.LoadPoliciesOrDefault(ctx, *policyFname, *challengeDifficulty, *slogLevel)
|
policy, err := libanubis.LoadPoliciesOrDefault(ctx, *policyFname, *challengeDifficulty, *slogLevel, strings.TrimSpace(*target) == "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("can't parse policy file: %v", err)
|
log.Fatalf("can't parse policy file: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
<!-- This changes the project to: -->
|
<!-- This changes the project to: -->
|
||||||
|
|
||||||
|
- Patch [GHSA-6wcg-mqvh-fcvg](https://github.com/TecharoHQ/anubis/security/advisories/GHSA-6wcg-mqvh-fcvg) by containing subrequest logic to Anubis instances in subrequest mode.
|
||||||
- Move metrics server configuration to [the policy file](./admin/policies.mdx#metrics-server).
|
- Move metrics server configuration to [the policy file](./admin/policies.mdx#metrics-server).
|
||||||
- Expose [pprof endpoints](https://pkg.go.dev/net/http/pprof) on the metrics listener to enable profiling Anubis in production.
|
- Expose [pprof endpoints](https://pkg.go.dev/net/http/pprof) on the metrics listener to enable profiling Anubis in production.
|
||||||
- fix: prevent nil pointer panic in challenge validation when threshold rules match during PassChallenge (#1463)
|
- fix: prevent nil pointer panic in challenge validation when threshold rules match during PassChallenge (#1463)
|
||||||
@@ -26,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Enable [HTTP basic auth](./admin/policies.mdx#http-basic-authentication) for the metrics server.
|
- Enable [HTTP basic auth](./admin/policies.mdx#http-basic-authentication) for the metrics server.
|
||||||
- Fix a bug in the dataset poisoning maze that could allow denial of service [#1580](https://github.com/TecharoHQ/anubis/issues/1580).
|
- Fix a bug in the dataset poisoning maze that could allow denial of service [#1580](https://github.com/TecharoHQ/anubis/issues/1580).
|
||||||
- Add config option to add ASN to logs/metrics.
|
- Add config option to add ASN to logs/metrics.
|
||||||
|
- Log weight when issuing challenge
|
||||||
|
- Fix `path_regex` and CEL `path` rules not matching when using Traefik `forwardAuth` middleware. Anubis now checks `X-Forwarded-Uri` (Traefik) in addition to `X-Original-URI` (nginx) when resolving the request path in subrequest mode ([#1628](https://github.com/TecharoHQ/anubis/issues/1628)).
|
||||||
|
|
||||||
## v1.25.0: Necron
|
## v1.25.0: Necron
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ require (
|
|||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||||
github.com/go-git/go-git/v5 v5.16.2 // indirect
|
github.com/go-git/go-git/v5 v5.16.2 // indirect
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
|||||||
@@ -189,8 +189,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
|
|||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
|
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
|
||||||
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
|||||||
@@ -595,7 +595,7 @@ func spawnAnubisWithOptions(t *testing.T, basePrefix string) string {
|
|||||||
fmt.Fprintf(w, "<html><body><span id=anubis-test>%d</span></body></html>", time.Now().Unix())
|
fmt.Fprintf(w, "<html><body><span id=anubis-test>%d</span></body></html>", time.Now().Unix())
|
||||||
})
|
})
|
||||||
|
|
||||||
policy, err := libanubis.LoadPoliciesOrDefault(t.Context(), "", anubis.DefaultDifficulty, "info")
|
policy, err := libanubis.LoadPoliciesOrDefault(t.Context(), "", anubis.DefaultDifficulty, "info", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -186,7 +186,7 @@ func (s *Server) issueChallenge(ctx context.Context, r *http.Request, lg *slog.L
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lg.Info("new challenge issued", "challenge", id.String())
|
lg.Info("new challenge issued", "challenge", id.String(), "weight", cr.Weight)
|
||||||
|
|
||||||
return &chall, err
|
return &chall, err
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -58,7 +58,7 @@ func loadPolicies(t *testing.T, fname string, difficulty int) *policy.ParsedConf
|
|||||||
|
|
||||||
t.Logf("loading policy file: %s", fname)
|
t.Logf("loading policy file: %s", fname)
|
||||||
|
|
||||||
anubisPolicy, err := LoadPoliciesOrDefault(ctx, fname, difficulty, "info")
|
anubisPolicy, err := LoadPoliciesOrDefault(ctx, fname, difficulty, "info", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -250,7 +250,7 @@ func TestLoadPolicies(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
if _, err := policy.ParseConfig(t.Context(), fin, fname, 4, "info"); err != nil {
|
if _, err := policy.ParseConfig(t.Context(), fin, fname, 4, "info", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-2
@@ -55,7 +55,7 @@ type Options struct {
|
|||||||
DifficultyInJWT bool
|
DifficultyInJWT bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int, logLevel string) (*policy.ParsedConfig, error) {
|
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int, logLevel string, subrequestMode bool) (*policy.ParsedConfig, error) {
|
||||||
var fin io.ReadCloser
|
var fin io.ReadCloser
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty
|
|||||||
}
|
}
|
||||||
}(fin)
|
}(fin)
|
||||||
|
|
||||||
anubisPolicy, err := policy.ParseConfig(ctx, fin, fname, defaultDifficulty, logLevel)
|
anubisPolicy, err := policy.ParseConfig(ctx, fin, fname, defaultDifficulty, logLevel, subrequestMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
|
return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestInvalidChallengeMethod(t *testing.T) {
|
func TestInvalidChallengeMethod(t *testing.T) {
|
||||||
if _, err := LoadPoliciesOrDefault(t.Context(), "testdata/invalid-challenge-method.yaml", 4, "info"); !errors.Is(err, policy.ErrChallengeRuleHasWrongAlgorithm) {
|
if _, err := LoadPoliciesOrDefault(t.Context(), "testdata/invalid-challenge-method.yaml", 4, "info", false); !errors.Is(err, policy.ErrChallengeRuleHasWrongAlgorithm) {
|
||||||
t.Fatalf("wanted error %v but got %v", policy.ErrChallengeRuleHasWrongAlgorithm, err)
|
t.Fatalf("wanted error %v but got %v", policy.ErrChallengeRuleHasWrongAlgorithm, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ func TestBadConfigs(t *testing.T) {
|
|||||||
|
|
||||||
for _, st := range finfos {
|
for _, st := range finfos {
|
||||||
t.Run(st.Name(), func(t *testing.T) {
|
t.Run(st.Name(), func(t *testing.T) {
|
||||||
if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "bad", st.Name()), anubis.DefaultDifficulty, "info"); err == nil {
|
if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "bad", st.Name()), anubis.DefaultDifficulty, "info", false); err == nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
@@ -44,13 +44,13 @@ func TestGoodConfigs(t *testing.T) {
|
|||||||
t.Run(st.Name(), func(t *testing.T) {
|
t.Run(st.Name(), func(t *testing.T) {
|
||||||
t.Run("with-thoth", func(t *testing.T) {
|
t.Run("with-thoth", func(t *testing.T) {
|
||||||
ctx := thothmock.WithMockThoth(t)
|
ctx := thothmock.WithMockThoth(t)
|
||||||
if _, err := LoadPoliciesOrDefault(ctx, filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info"); err != nil {
|
if _, err := LoadPoliciesOrDefault(ctx, filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("without-thoth", func(t *testing.T) {
|
t.Run("without-thoth", func(t *testing.T) {
|
||||||
if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info"); err != nil {
|
if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CELChecker struct {
|
type CELChecker struct {
|
||||||
program cel.Program
|
program cel.Program
|
||||||
src string
|
src string
|
||||||
|
subRequestMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns) (*CELChecker, error) {
|
func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns, subRequestMode bool) (*CELChecker, error) {
|
||||||
env, err := expressions.BotEnvironment(dnsObj)
|
env, err := expressions.BotEnvironment(dnsObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -29,8 +30,9 @@ func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns) (*CELChecker,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &CELChecker{
|
return &CELChecker{
|
||||||
src: cfg.String(),
|
src: cfg.String(),
|
||||||
program: program,
|
program: program,
|
||||||
|
subRequestMode: subRequestMode,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ func (cc *CELChecker) Hash() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cc *CELChecker) Check(r *http.Request) (bool, error) {
|
func (cc *CELChecker) Check(r *http.Request) (bool, error) {
|
||||||
result, _, err := cc.program.ContextEval(r.Context(), &CELRequest{r})
|
result, _, err := cc.program.ContextEval(r.Context(), &CELRequest{r, cc.subRequestMode})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -54,6 +56,7 @@ func (cc *CELChecker) Check(r *http.Request) (bool, error) {
|
|||||||
|
|
||||||
type CELRequest struct {
|
type CELRequest struct {
|
||||||
*http.Request
|
*http.Request
|
||||||
|
subRequestMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr *CELRequest) Parent() cel.Activation { return nil }
|
func (cr *CELRequest) Parent() cel.Activation { return nil }
|
||||||
@@ -71,6 +74,14 @@ func (cr *CELRequest) ResolveName(name string) (any, bool) {
|
|||||||
case "userAgent":
|
case "userAgent":
|
||||||
return cr.UserAgent(), true
|
return cr.UserAgent(), true
|
||||||
case "path":
|
case "path":
|
||||||
|
if cr.subRequestMode {
|
||||||
|
if xou := cr.Header.Get("X-Original-URI"); xou != "" {
|
||||||
|
return xou, true
|
||||||
|
}
|
||||||
|
if xfu := cr.Header.Get("X-Forwarded-Uri"); xfu != "" {
|
||||||
|
return xfu, true
|
||||||
|
}
|
||||||
|
}
|
||||||
return cr.URL.Path, true
|
return cr.URL.Path, true
|
||||||
case "query":
|
case "query":
|
||||||
return expressions.URLValues{Values: cr.URL.Query()}, true
|
return expressions.URLValues{Values: cr.URL.Query()}, true
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestCELChecker_MapIterationWrappers(t *testing.T) {
|
|||||||
Expression: `headers.exists(k, k == "Accept") && query.exists(k, k == "format")`,
|
Expression: `headers.exists(k, k == "Accept") && query.exists(k, k == "format")`,
|
||||||
}
|
}
|
||||||
|
|
||||||
checker, err := NewCELChecker(cfg, newTestDNS(t))
|
checker, err := NewCELChecker(cfg, newTestDNS(t), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("creating CEL checker failed: %v", err)
|
t.Fatalf("creating CEL checker failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -42,3 +42,77 @@ func TestCELChecker_MapIterationWrappers(t *testing.T) {
|
|||||||
t.Fatal("expected expression to evaluate true")
|
t.Fatal("expected expression to evaluate true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCELChecker_PathWithForwardedUri(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expression string
|
||||||
|
xForwardedUri string
|
||||||
|
urlPath string
|
||||||
|
subRequestMode bool
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "path matches X-Forwarded-Uri in subrequest mode",
|
||||||
|
expression: `path.startsWith("/admin")`,
|
||||||
|
xForwardedUri: "/admin/secret",
|
||||||
|
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path with query string",
|
||||||
|
expression: `path.startsWith("/api/secret")`,
|
||||||
|
xForwardedUri: "/api/secret?token=abc",
|
||||||
|
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path falls back to url path when no header",
|
||||||
|
expression: `path == "/public/page"`,
|
||||||
|
urlPath: "/public/page",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-subrequest mode ignores X-Forwarded-Uri",
|
||||||
|
expression: `path.startsWith("/admin")`,
|
||||||
|
xForwardedUri: "/admin/secret",
|
||||||
|
urlPath: "/public/page",
|
||||||
|
subRequestMode: false,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cfg := &config.ExpressionOrList{
|
||||||
|
Expression: tt.expression,
|
||||||
|
}
|
||||||
|
checker, err := NewCELChecker(cfg, newTestDNS(t), tt.subRequestMode)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewCELChecker() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://example.com"+tt.urlPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("http.NewRequest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.xForwardedUri != "" {
|
||||||
|
req.Header.Set("X-Forwarded-Uri", tt.xForwardedUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := checker.Check(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Check() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("Check() = %v, want %v (subRequestMode=%v, urlPath=%q, X-Forwarded-Uri=%q)",
|
||||||
|
got, tt.want, tt.subRequestMode, tt.urlPath, tt.xForwardedUri)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+14
-8
@@ -94,23 +94,29 @@ func (hmc *HeaderMatchesChecker) Hash() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PathChecker struct {
|
type PathChecker struct {
|
||||||
regexp *regexp.Regexp
|
regexp *regexp.Regexp
|
||||||
hash string
|
hash string
|
||||||
|
subRequestMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPathChecker(rexStr string) (checker.Impl, error) {
|
func NewPathChecker(rexStr string, subrequestMode bool) (checker.Impl, error) {
|
||||||
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: regex %s failed parse: %w", ErrMisconfiguration, rexStr, err)
|
return nil, fmt.Errorf("%w: regex %s failed parse: %w", ErrMisconfiguration, rexStr, err)
|
||||||
}
|
}
|
||||||
return &PathChecker{rex, internal.FastHash(rexStr)}, nil
|
return &PathChecker{rex, internal.FastHash(rexStr), subrequestMode}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pc *PathChecker) Check(r *http.Request) (bool, error) {
|
func (pc *PathChecker) Check(r *http.Request) (bool, error) {
|
||||||
originalUrl := r.Header.Get("X-Original-URI")
|
if pc.subRequestMode {
|
||||||
if originalUrl != "" {
|
originalUrl := r.Header.Get("X-Original-URI")
|
||||||
if pc.regexp.MatchString(originalUrl) {
|
if originalUrl == "" {
|
||||||
return true, nil
|
originalUrl = r.Header.Get("X-Forwarded-Uri")
|
||||||
|
}
|
||||||
|
if originalUrl != "" {
|
||||||
|
if pc.regexp.MatchString(originalUrl) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+223
-2
@@ -272,8 +272,8 @@ func TestPathChecker_XOriginalURI(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Create the PathChecker
|
// Create the PathChecker in subrequest mode so X-Original-URI is honored.
|
||||||
pc, err := NewPathChecker(tt.regex)
|
pc, err := NewPathChecker(tt.regex, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !tt.expectError {
|
if !tt.expectError {
|
||||||
t.Fatalf("NewPathChecker() unexpected error: %v", err)
|
t.Fatalf("NewPathChecker() unexpected error: %v", err)
|
||||||
@@ -305,3 +305,224 @@ func TestPathChecker_XOriginalURI(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPathChecker_GHSA_6wcg_mqvh_fcvg is a regression test for
|
||||||
|
// https://github.com/TecharoHQ/anubis/security/advisories/GHSA-6wcg-mqvh-fcvg.
|
||||||
|
//
|
||||||
|
// PR https://github.com/TecharoHQ/anubis/pull/1015 added the ability for
|
||||||
|
// reverse proxies using Anubis in subrequest auth mode to look at the path
|
||||||
|
// of a request as there are many rules in the wild that rely on checking
|
||||||
|
// the path. This is how access to things like robots.txt or anything in the
|
||||||
|
// .well-known directory is unaffected by Anubis.
|
||||||
|
//
|
||||||
|
// However this logic was also enabled for non-subrequest deployments of Anubis,
|
||||||
|
// meaning that a specially crafted request could include a /.well-known/
|
||||||
|
// path in it and then get around Anubis with little effort.
|
||||||
|
//
|
||||||
|
// This fix gates the logic behind a new plumbed variable named subrequestMode
|
||||||
|
// that only fires when Anubis is running in subrequest auth mode. This
|
||||||
|
// properly contains that workaround so that the logic does not fire in
|
||||||
|
// most deployments.
|
||||||
|
func TestPathChecker_GHSA_6wcg_mqvh_fcvg(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
regex string
|
||||||
|
urlPath string
|
||||||
|
xOriginalURI string
|
||||||
|
subRequestMode bool
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default mode ignores spoofed X-Original-URI when real path matches",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/admin/secret",
|
||||||
|
xOriginalURI: "/public/index",
|
||||||
|
subRequestMode: false,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default mode ignores spoofed X-Original-URI when real path does not match",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/public/index",
|
||||||
|
xOriginalURI: "/admin/secret",
|
||||||
|
subRequestMode: false,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default mode without X-Original-URI matches real path",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/admin/dashboard",
|
||||||
|
xOriginalURI: "",
|
||||||
|
subRequestMode: false,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subrequest mode honors X-Original-URI",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/auth",
|
||||||
|
xOriginalURI: "/admin/secret",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subrequest mode falls back to URL.Path when X-Original-URI does not match",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/admin/dashboard",
|
||||||
|
xOriginalURI: "/public/index",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subrequest mode with empty X-Original-URI uses URL.Path",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/admin/dashboard",
|
||||||
|
xOriginalURI: "",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
pc, err := NewPathChecker(tt.regex, tt.subRequestMode)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPathChecker(%q, %v) returned error: %v", tt.regex, tt.subRequestMode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://example.com"+tt.urlPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("http.NewRequest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.xOriginalURI != "" {
|
||||||
|
req.Header.Set("X-Original-URI", tt.xOriginalURI)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := pc.Check(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Check() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("Check() = %v, want %v (subRequestMode=%v, urlPath=%q, X-Original-URI=%q)",
|
||||||
|
got, tt.want, tt.subRequestMode, tt.urlPath, tt.xOriginalURI)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathChecker_XForwardedUri(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
regex string
|
||||||
|
xForwardedUri string
|
||||||
|
xOriginalURI string
|
||||||
|
urlPath string
|
||||||
|
subRequestMode bool
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "X-Forwarded-Uri matches regex in subrequest mode",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
xForwardedUri: "/admin/users",
|
||||||
|
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "X-Forwarded-Uri with query string",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
xForwardedUri: "/admin/users?page=1",
|
||||||
|
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "X-Original-URI takes priority over X-Forwarded-Uri",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
xForwardedUri: "/public/page",
|
||||||
|
xOriginalURI: "/admin/users",
|
||||||
|
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "falls back to X-Forwarded-Uri when no X-Original-URI",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
xForwardedUri: "/admin/dashboard",
|
||||||
|
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "neither header matches, url path matches",
|
||||||
|
regex: "^/public/.*",
|
||||||
|
xForwardedUri: "/admin/users",
|
||||||
|
urlPath: "/public/page",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nothing matches",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
xForwardedUri: "/public/page",
|
||||||
|
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-subrequest mode ignores X-Forwarded-Uri",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
xForwardedUri: "/admin/users",
|
||||||
|
urlPath: "/public/page",
|
||||||
|
subRequestMode: false,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-subrequest mode uses url path",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
xForwardedUri: "/public/page",
|
||||||
|
urlPath: "/admin/secret",
|
||||||
|
subRequestMode: false,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty X-Forwarded-Uri falls back to url path",
|
||||||
|
regex: "^/check$",
|
||||||
|
urlPath: "/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
pc, err := NewPathChecker(tt.regex, tt.subRequestMode)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPathChecker(%q, %v) returned error: %v", tt.regex, tt.subRequestMode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://example.com"+tt.urlPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("http.NewRequest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.xForwardedUri != "" {
|
||||||
|
req.Header.Set("X-Forwarded-Uri", tt.xForwardedUri)
|
||||||
|
}
|
||||||
|
if tt.xOriginalURI != "" {
|
||||||
|
req.Header.Set("X-Original-URI", tt.xOriginalURI)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := pc.Check(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Check() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("Check() = %v, want %v (subRequestMode=%v, urlPath=%q, X-Forwarded-Uri=%q, X-Original-URI=%q)",
|
||||||
|
got, tt.want, tt.subRequestMode, tt.urlPath, tt.xForwardedUri, tt.xOriginalURI)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func newParsedConfig(orig *config.Config) *ParsedConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDifficulty int, logLevel string) (*ParsedConfig, error) {
|
func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDifficulty int, logLevel string, subrequestMode bool) (*ParsedConfig, error) {
|
||||||
c, err := config.Load(fin, fname)
|
c, err := config.Load(fin, fname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -152,7 +152,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
}
|
}
|
||||||
|
|
||||||
if b.PathRegex != nil {
|
if b.PathRegex != nil {
|
||||||
c, err := NewPathChecker(*b.PathRegex)
|
c, err := NewPathChecker(*b.PathRegex, subrequestMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s path regex: %w", b.Name, err))
|
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s path regex: %w", b.Name, err))
|
||||||
} else {
|
} else {
|
||||||
@@ -170,7 +170,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
}
|
}
|
||||||
|
|
||||||
if b.Expression != nil {
|
if b.Expression != nil {
|
||||||
c, err := NewCELChecker(b.Expression, result.Dns)
|
c, err := NewCELChecker(b.Expression, result.Dns, subrequestMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err))
|
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func TestDefaultPolicyMustParse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
if _, err := ParseConfig(ctx, fin, "botPolicies.yaml", anubis.DefaultDifficulty, "info"); err != nil {
|
if _, err := ParseConfig(ctx, fin, "botPolicies.yaml", anubis.DefaultDifficulty, "info", false); err != nil {
|
||||||
t.Fatalf("can't parse config: %v", err)
|
t.Fatalf("can't parse config: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ func TestGoodConfigs(t *testing.T) {
|
|||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
ctx := thothmock.WithMockThoth(t)
|
ctx := thothmock.WithMockThoth(t)
|
||||||
if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info"); err != nil {
|
if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -53,7 +53,7 @@ func TestGoodConfigs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
if _, err := ParseConfig(t.Context(), fin, fin.Name(), anubis.DefaultDifficulty, "info"); err != nil {
|
if _, err := ParseConfig(t.Context(), fin, fin.Name(), anubis.DefaultDifficulty, "info", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -77,7 +77,7 @@ func TestBadConfigs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info"); err == nil {
|
if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info", false); err == nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
|
|||||||
Generated
+324
-357
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -20,11 +20,11 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^20.5.0",
|
"@commitlint/cli": "^20.5.3",
|
||||||
"@commitlint/config-conventional": "^20.5.0",
|
"@commitlint/config-conventional": "^20.5.3",
|
||||||
"baseline-browser-mapping": "^2.10.15",
|
"baseline-browser-mapping": "^2.10.27",
|
||||||
"cssnano": "^7.1.4",
|
"cssnano": "^7.1.8",
|
||||||
"cssnano-preset-advanced": "^7.0.12",
|
"cssnano-preset-advanced": "^7.0.16",
|
||||||
"esbuild": "^0.28.0",
|
"esbuild": "^0.28.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"playwright": "^1.52.0",
|
"playwright": "^1.52.0",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"postcss-import": "^16.1.1",
|
"postcss-import": "^16.1.1",
|
||||||
"postcss-import-url": "^7.2.0",
|
"postcss-import-url": "^7.2.0",
|
||||||
"postcss-url": "^10.1.3",
|
"postcss-url": "^10.1.3",
|
||||||
"prettier": "^3.8.1"
|
"prettier": "^3.8.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-crypto/sha256-js": "^5.2.0",
|
"@aws-crypto/sha256-js": "^5.2.0",
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
bots:
|
||||||
|
- name: block-admin-via-regex
|
||||||
|
path_regex: ^/admin(/.*)?$
|
||||||
|
action: DENY
|
||||||
|
|
||||||
|
- name: block-secret-via-cel
|
||||||
|
expression:
|
||||||
|
all:
|
||||||
|
- 'path.startsWith("/api/secret")'
|
||||||
|
action: DENY
|
||||||
|
|
||||||
|
- import: (data)/meta/default-config.yaml
|
||||||
|
|
||||||
|
status_codes:
|
||||||
|
CHALLENGE: 200
|
||||||
|
DENY: 403
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v3.3
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 8080:80
|
||||||
|
volumes:
|
||||||
|
- ./traefik.yml:/etc/traefik/traefik.yml:ro
|
||||||
|
- ./http.yaml:/config/http.yaml:ro
|
||||||
|
|
||||||
|
anubis:
|
||||||
|
image: ko.local/anubis
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
BIND: ":8080"
|
||||||
|
TARGET: " "
|
||||||
|
POLICY_FNAME: /etc/techaro/anubis.yaml
|
||||||
|
PUBLIC_URL: http://localhost:8080/.within.website/x/cmd/anubis
|
||||||
|
COOKIE_DOMAIN: localhost
|
||||||
|
USE_REMOTE_ADDRESS: "true"
|
||||||
|
volumes:
|
||||||
|
- ./anubis.yaml:/etc/techaro/anubis.yaml
|
||||||
|
|
||||||
|
backend:
|
||||||
|
image: ghcr.io/xe/x/httpdebug
|
||||||
|
pull_policy: always
|
||||||
|
restart: always
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
anubis:
|
||||||
|
forwardAuth:
|
||||||
|
address: http://anubis:8080/.within.website/x/cmd/anubis/api/check
|
||||||
|
trustForwardHeader: true
|
||||||
|
|
||||||
|
routers:
|
||||||
|
anubis-assets:
|
||||||
|
rule: Host(`localhost`) && PathPrefix(`/.within.website/x/cmd/anubis`)
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
service: anubis
|
||||||
|
backend:
|
||||||
|
rule: Host(`localhost`)
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
service: backend
|
||||||
|
middlewares:
|
||||||
|
- anubis
|
||||||
|
|
||||||
|
services:
|
||||||
|
anubis:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://anubis:8080
|
||||||
|
backend:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://backend:3000
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Smoke test for https://github.com/TecharoHQ/anubis/issues/1628
|
||||||
|
//
|
||||||
|
// Traefik's forwardAuth middleware calls Anubis at the literal path
|
||||||
|
// /.within.website/x/cmd/anubis/api/check and conveys the original URL in the
|
||||||
|
// X-Forwarded-Uri header. Path-targeting policy rules must match that header
|
||||||
|
// (not r.URL.Path), otherwise every request looks like a request to /check.
|
||||||
|
|
||||||
|
const BASE = "http://localhost:8080";
|
||||||
|
const UA = "Mozilla/5.0 (compatible; AnubisTraefikSmoke/1.0)";
|
||||||
|
|
||||||
|
const cases = [
|
||||||
|
{ path: "/", expected: 307, why: "control: no DENY rule, default challenge redirect" },
|
||||||
|
{ path: "/free", expected: 307, why: "control: no DENY rule, default challenge redirect" },
|
||||||
|
{ path: "/admin", expected: 403, why: "path_regex must match X-Forwarded-Uri, not 307 or 200" },
|
||||||
|
{ path: "/admin/users", expected: 403, why: "path_regex must match X-Forwarded-Uri, not 307 or 200" },
|
||||||
|
{ path: "/api/secret", expected: 403, why: "CEL path must match X-Forwarded-Uri, not 307 or 200" },
|
||||||
|
];
|
||||||
|
|
||||||
|
let failed = false;
|
||||||
|
|
||||||
|
for (const c of cases) {
|
||||||
|
const resp = await fetch(`${BASE}${c.path}`, {
|
||||||
|
headers: { "User-Agent": UA },
|
||||||
|
redirect: "manual",
|
||||||
|
});
|
||||||
|
const ok = resp.status === c.expected;
|
||||||
|
console.log(
|
||||||
|
`${ok ? "PASS" : "FAIL"}: GET ${c.path} → ${resp.status} (want ${c.expected}: ${c.why})`,
|
||||||
|
);
|
||||||
|
if (!ok) failed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(failed ? 1 : 0);
|
||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
export VERSION=${GITHUB_SHA:-devel}-test
|
||||||
|
export KO_DOCKER_REPO=ko.local
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
source ../lib/lib.sh
|
||||||
|
|
||||||
|
build_anubis_ko
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
docker compose down -t 1 || :
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT SIGINT
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
backoff-retry --try-count 20 node ./test.mjs
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
|
|
||||||
|
providers:
|
||||||
|
file:
|
||||||
|
directory: /config
|
||||||
|
watch: false
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
Reference in New Issue
Block a user