fix(security): resolve Security-004..007 — configurable user-id attribute, DN escaping, JWT issuer/audience validation, idle-timeout preservation
This commit is contained in:
@@ -71,7 +71,7 @@ public class LdapAuthService
|
||||
|
||||
try
|
||||
{
|
||||
var searchFilter = $"(uid={EscapeLdapFilter(username)})";
|
||||
var searchFilter = $"({_options.LdapUserIdAttribute}={EscapeLdapFilter(username)})";
|
||||
var searchResults = await Task.Run(() =>
|
||||
connection.Search(
|
||||
_options.LdapSearchBase,
|
||||
@@ -133,17 +133,13 @@ public class LdapAuthService
|
||||
/// </summary>
|
||||
private async Task<string> ResolveUserDnAsync(LdapConnection connection, string username, CancellationToken ct)
|
||||
{
|
||||
// If username already looks like a DN, use it as-is
|
||||
if (username.Contains('='))
|
||||
return username;
|
||||
|
||||
// If a service account is configured, search for the user's actual DN
|
||||
if (!string.IsNullOrWhiteSpace(_options.LdapServiceAccountDn))
|
||||
{
|
||||
await Task.Run(() =>
|
||||
connection.Bind(_options.LdapServiceAccountDn, _options.LdapServiceAccountPassword), ct);
|
||||
|
||||
var searchFilter = $"(uid={EscapeLdapFilter(username)})";
|
||||
var searchFilter = $"({_options.LdapUserIdAttribute}={EscapeLdapFilter(username)})";
|
||||
var searchResults = await Task.Run(() =>
|
||||
connection.Search(
|
||||
_options.LdapSearchBase,
|
||||
@@ -158,13 +154,68 @@ public class LdapAuthService
|
||||
return entry.Dn;
|
||||
}
|
||||
|
||||
throw new LdapException("User not found", LdapException.NoSuchObject, $"No entry found for uid={username}");
|
||||
throw new LdapException("User not found", LdapException.NoSuchObject,
|
||||
$"No entry found for {_options.LdapUserIdAttribute}={username}");
|
||||
}
|
||||
|
||||
// Fallback: construct DN directly
|
||||
return string.IsNullOrWhiteSpace(_options.LdapSearchBase)
|
||||
? $"cn={username}"
|
||||
: $"cn={username},{_options.LdapSearchBase}";
|
||||
// Fallback: construct the bind DN directly from the configured user-id
|
||||
// attribute. The username is RFC 4514 DN-escaped so it cannot alter the
|
||||
// DN structure (Security-005). The previous Contains('=') shortcut that
|
||||
// accepted a raw caller-supplied DN has been removed — accepting an
|
||||
// arbitrary DN from untrusted input let a client choose the bind identity.
|
||||
return BuildFallbackUserDn(username, _options.LdapSearchBase, _options.LdapUserIdAttribute);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the no-service-account fallback bind DN as
|
||||
/// <c>{userIdAttribute}={escaped-username}[,{searchBase}]</c>. The username is
|
||||
/// escaped per RFC 4514 so DN metacharacters in untrusted input cannot inject
|
||||
/// additional RDN components or change the bind identity.
|
||||
/// </summary>
|
||||
public static string BuildFallbackUserDn(string username, string searchBase, string userIdAttribute)
|
||||
{
|
||||
var rdn = $"{userIdAttribute}={EscapeLdapDn(username)}";
|
||||
return string.IsNullOrWhiteSpace(searchBase) ? rdn : $"{rdn},{searchBase}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes a string for use as an RFC 4514 DN attribute value: the special
|
||||
/// characters <c>, + " \ < > ;</c> are backslash-escaped, as are a leading
|
||||
/// or trailing space and a leading <c>#</c>.
|
||||
/// </summary>
|
||||
public static string EscapeLdapDn(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
return input;
|
||||
|
||||
var sb = new System.Text.StringBuilder(input.Length + 8);
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
var c = input[i];
|
||||
var isEdgeSpace = c == ' ' && (i == 0 || i == input.Length - 1);
|
||||
var isLeadingHash = c == '#' && i == 0;
|
||||
switch (c)
|
||||
{
|
||||
case ',':
|
||||
case '+':
|
||||
case '"':
|
||||
case '\\':
|
||||
case '<':
|
||||
case '>':
|
||||
case ';':
|
||||
sb.Append('\\').Append(c);
|
||||
break;
|
||||
case '\0':
|
||||
sb.Append("\\00");
|
||||
break;
|
||||
default:
|
||||
if (isEdgeSpace || isLeadingHash)
|
||||
sb.Append('\\');
|
||||
sb.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string EscapeLdapFilter(string input)
|
||||
|
||||
Reference in New Issue
Block a user