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
@@ -5,8 +5,9 @@ namespace ZB.MOM.WW.OtOpcUa.Security.Ldap;
/// <summary>
/// LDAP + role-mapping configuration for the Admin UI. Bound from <c>appsettings.json</c>
/// <c>Security:Ldap</c> section. Defaults point at the local GLAuth dev instance (see
/// <c>C:\publish\glauth\auth.md</c>).
/// <c>Security:Ldap</c> section. Defaults point at the shared GLAuth dev instance on the
/// Linux Docker host (<c>10.100.0.35:3893</c>) — see <c>docs/security.md</c> §"LDAP bind
/// flow" and <c>CLAUDE.md</c> §"LDAP Authentication" for setup details.
/// </summary>
/// <remarks>
/// Carries both the wire fields the shared <c>ZB.MOM.WW.Auth.Ldap</c> directory client needs