From d40e9056bc75dec24b45b2892037b62f6e5a1428 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Thu, 24 Jul 2025 10:05:00 -0400 Subject: [PATCH] fix(lib): block XSS attacks via nonstandard URLs (#904) * fix(lib): block XSS attacks via nonstandard URLs This could allow an attacker to craft an Anubis pass-challenge URL that forces a redirect to nonstandard URLs, such as the `javascript:` scheme which executes arbitrary JavaScript code in a browser context when the user clicks the "Try again" button. Release-status: cut Signed-off-by: Xe Iaso * chore: spelling Signed-off-by: Xe Iaso --------- Signed-off-by: Xe Iaso --- .github/actions/spelling/allow.txt | 3 +- .github/actions/spelling/patterns.txt | 4 ++ docs/docs/CHANGELOG.md | 8 +++ lib/anubis.go | 8 +++ lib/anubis_test.go | 76 +++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 1 deletion(-) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 64dd9a9..c220908 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -3,4 +3,5 @@ https ssh ubuntu workarounds -rjack \ No newline at end of file +rjack +msgbox \ No newline at end of file diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt index 0296702..684f515 100644 --- a/.github/actions/spelling/patterns.txt +++ b/.github/actions/spelling/patterns.txt @@ -132,3 +132,7 @@ go install(?:\s+[a-z]+\.[-@\w/.]+)+ # hit-count: 1 file-count: 1 # microsoft \b(?:https?://|)(?:(?:(?:blogs|download\.visualstudio|docs|msdn2?|research)\.|)microsoft|blogs\.msdn)\.co(?:m|\.\w\w)/[-_a-zA-Z0-9()=./%]* + +# hit-count: 1 file-count: 1 +# data url +\bdata:[-a-zA-Z=;:/0-9+]*,\S* \ No newline at end of file diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index a0806e9..5ea384d 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -13,6 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +### Fixes + +#### Fixes a problem with nonstandard URLs and redirects + +This could allow an attacker to craft an Anubis pass-challenge URL that forces a redirect to nonstandard URLs, such as the `javascript:` scheme which executes arbitrary JavaScript code in a browser context when the user clicks the "Try again" button. + +This has been fixed by disallowing any URLs without the scheme `http` or `https`. + ## v1.21.1: Minfilia Warde - Echo 1 - Expired records are now properly removed from bbolt databases ([#848](https://github.com/TecharoHQ/anubis/pull/848)). diff --git a/lib/anubis.go b/lib/anubis.go index 2a6e520..fbf1b5c 100644 --- a/lib/anubis.go +++ b/lib/anubis.go @@ -399,12 +399,20 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { } redir := r.FormValue("redir") + redirURL, err := url.ParseRequestURI(redir) if err != nil { lg.Error("invalid redirect", "err", err) s.respondWithError(w, r, localizer.T("invalid_redirect")) return } + + if redirURL.Scheme != "" && redirURL.Scheme != "http" && redirURL.Scheme != "https" { + lg.Error("XSS attempt blocked, invalid redirect scheme", "scheme", redirURL.Scheme) + s.respondWithStatus(w, r, localizer.T("invalid_redirect"), http.StatusBadRequest) + return + } + // used by the path checker rule r.URL = redirURL diff --git a/lib/anubis_test.go b/lib/anubis_test.go index 056793a..c4fa136 100644 --- a/lib/anubis_test.go +++ b/lib/anubis_test.go @@ -801,3 +801,79 @@ func TestChallengeFor_ErrNotFound(t *testing.T) { } }) } + +func TestPassChallengeXSS(t *testing.T) { + pol := loadPolicies(t, "", anubis.DefaultDifficulty) + + srv := spawnAnubis(t, Options{ + Next: http.NewServeMux(), + Policy: pol, + }) + + ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv)) + defer ts.Close() + + cli := httpClient(t) + chall := makeChallenge(t, ts, cli) + + testCases := []struct { + name string + redir string + }{ + { + name: "javascript alert", + redir: "javascript:alert('xss')", + }, + { + name: "vbscript", + redir: "vbscript:msgbox(\"XSS\")", + }, + { + name: "data url", + redir: "data:text/html;base64,PHNjcmlwdD5hbGVydCgneHNzJyk8L3NjcmlwdD4=", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + nonce := 0 + elapsedTime := 420 + calculated := "" + calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce) + calculated = internal.SHA256sum(calcString) + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil) + if err != nil { + t.Fatalf("can't make request: %v", err) + } + + q := req.URL.Query() + q.Set("response", calculated) + q.Set("nonce", fmt.Sprint(nonce)) + q.Set("redir", tc.redir) + q.Set("elapsedTime", fmt.Sprint(elapsedTime)) + req.URL.RawQuery = q.Encode() + + u, err := url.Parse(ts.URL) + if err != nil { + t.Fatal(err) + } + + for _, ckie := range cli.Jar.Cookies(u) { + if ckie.Name == anubis.TestCookieName { + req.AddCookie(ckie) + } + } + + resp, err := cli.Do(req) + if err != nil { + t.Fatalf("can't do request: %v", err) + } + + if resp.StatusCode != http.StatusBadRequest { + body, _ := io.ReadAll(resp.Body) + t.Errorf("wanted status %d, got %d. body: %s", http.StatusBadRequest, resp.StatusCode, body) + } + }) + } +}