fix(lib): close open redirect when in subrequest mode (#1222)

* test(nginx-external-auth): bring up to code standards

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(lib): close open redirect when in subrequest mode

Closes GHSA-cf57-c578-7jvv

Previously Anubis had an open redirect in subrequest auth mode due to an
insufficent fix in GHSA-jhjj-2g64-px7c. This patch adds additional
validation at several steps of the flow to prevent open redirects in
subrequest auth mode as well as implements automated testing to prevent
this from occuring in the future.

* docs: update CHANGELOG

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
Xe Iaso 2025-10-29 16:07:31 -04:00 committed by GitHub
parent 3dab060bfa
commit 7ed1753fcc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 384 additions and 64 deletions

View file

@ -296,6 +296,16 @@ func (s *Server) constructRedirectURL(r *http.Request) (string, error) {
if proto == "" || host == "" || uri == "" {
return "", errors.New(localizer.T("missing_required_forwarded_headers"))
}
switch proto {
case "http", "https":
// allowed
default:
lg := internal.GetRequestLogger(s.logger, r)
lg.Warn("invalid protocol in X-Forwarded-Proto", "proto", proto)
return "", errors.New(localizer.T("invalid_redirect"))
}
// Check if host is allowed in RedirectDomains (supports '*' via glob)
if len(s.opts.RedirectDomains) > 0 && !matchRedirectDomain(s.opts.RedirectDomains, host) {
lg := internal.GetRequestLogger(s.logger, r)
@ -369,16 +379,31 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
localizer := localization.GetLocalizer(r)
redir := r.FormValue("redir")
urlParsed, err := r.URL.Parse(redir)
urlParsed, err := url.ParseRequestURI(redir)
if err != nil {
s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), makeCode(err), http.StatusBadRequest)
// if ParseRequestURI fails, try as relative URL
urlParsed, err = r.URL.Parse(redir)
if err != nil {
s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), makeCode(err), http.StatusBadRequest)
return
}
}
// validate URL scheme to prevent javascript:, data:, file:, tel:, etc.
switch urlParsed.Scheme {
case "", "http", "https":
// allowed: empty scheme means relative URL
default:
lg := internal.GetRequestLogger(s.logger, r)
lg.Warn("XSS attempt blocked, invalid redirect scheme", "scheme", urlParsed.Scheme, "redir", redir)
s.respondWithStatus(w, r, localizer.T("invalid_redirect"), "", http.StatusBadRequest)
return
}
hostNotAllowed := len(urlParsed.Host) > 0 &&
len(s.opts.RedirectDomains) != 0 &&
!matchRedirectDomain(s.opts.RedirectDomains, urlParsed.Host)
hostMismatch := r.URL.Host != "" && urlParsed.Host != r.URL.Host
hostMismatch := r.URL.Host != "" && urlParsed.Host != "" && urlParsed.Host != r.URL.Host
if hostNotAllowed || hostMismatch {
lg := internal.GetRequestLogger(s.logger, r)