review(Security): fix login open-redirect (High) + stale LDAP doc

Code review at HEAD 7286d320. Security-001 (High): guard returnUrl with a local-URL
check before redirect (open-redirect/phishing vector) + regression test. Security-002:
update stale LdapOptions dev-LDAP doc reference.
This commit is contained in:
Joseph Doherty
2026-06-19 10:22:59 -04:00
parent b1946194d6
commit d23e585cdb
4 changed files with 118 additions and 3 deletions
@@ -132,9 +132,25 @@ public static class AuthEndpoints
await http.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
if (!isForm) return Results.NoContent();
return Results.Redirect(string.IsNullOrWhiteSpace(returnUrl) ? "/" : returnUrl);
// Security-001: validate returnUrl is a relative (local) URL before redirecting.
// An attacker can supply an absolute URL (e.g. https://evil.com) via a crafted link;
// LoginCard echoes the value verbatim into the hidden form field. Fall back to the
// app root rather than following the external URL (open-redirect prevention).
var destination = !string.IsNullOrWhiteSpace(returnUrl) && IsLocalUrl(returnUrl) ? returnUrl : "/";
return Results.Redirect(destination);
}
/// <summary>
/// Returns <see langword="true"/> when <paramref name="url"/> is a relative (local) URL
/// safe to redirect to after login. Rejects absolute URLs (with a scheme, e.g.
/// <c>https://evil.com</c>), protocol-relative URLs (<c>//evil.com</c>), and
/// anything that is not a well-formed relative URI — preventing open-redirect attacks
/// via a crafted <c>returnUrl</c> query parameter.
/// </summary>
/// <param name="url">The candidate redirect target.</param>
private static bool IsLocalUrl(string url) =>
Uri.IsWellFormedUriString(url, UriKind.Relative) && !url.StartsWith("//");
/// <summary>
/// Case-insensitive set-union of two role lists, preserving the de-duplication semantics the
/// legacy <c>RoleMapper.Merge</c> applied. Used to fold any pre-resolved roles (the DevStub