Fix open-redirect token leak and related auth hardening

Security review fixes:
- Constrain OAuth connect returnUrl to a site-relative path so the
  redeemable token_key can't be redirected off-domain (was a refresh-
  token leak / connection hijack)
- Route all login redirects (entra/dev/local) through ToLocalReturnUrl,
  also closing a protocol-relative // open redirect in local-login
- Neutralize CSV formula prefixes in both audit-log exporters via
  CsvSanitizer
- Force Secure flag on the prod auth cookie (Always, not SameAsRequest)
- Gate admin pages with an app_role-claim "Admin" policy instead of a
  render-time check

Findings and rationale recorded in SECURITY-TODO.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 11:39:20 +02:00
parent fe33960c0e
commit 17f6010a93
7 changed files with 105 additions and 29 deletions
+14
View File
@@ -4,4 +4,18 @@ public static class StringExtensions
{
public static string? TrimOrNull(this string? s)
=> string.IsNullOrWhiteSpace(s) ? null : s.Trim();
/// <summary>
/// Returns <paramref name="returnUrl"/> only when it is a safe site-relative path,
/// otherwise "/". Rejects absolute URLs and protocol-relative paths ("//evil.com",
/// "/\evil.com") so a post-auth / post-connect redirect can never leave the app.
/// Used by every login and OAuth-connect redirect to prevent open redirects.
/// </summary>
public static string ToLocalReturnUrl(this string? returnUrl)
{
if (string.IsNullOrEmpty(returnUrl)) return "/";
if (returnUrl[0] != '/') return "/"; // not site-relative
if (returnUrl.Length > 1 && (returnUrl[1] == '/' || returnUrl[1] == '\\')) return "/"; // protocol-relative
return returnUrl;
}
}