fix(security): resolve Security-012..015 — fail login on partial LDAP outage, escape-aware DN parsing, idle check on refresh, username normalization
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
| Last reviewed | 2026-05-17 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `39d737e` |
|
||||
| Open findings | 4 (1 deferred — Security-008) |
|
||||
| Open findings | 0 (1 deferred — Security-008) |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -483,7 +483,7 @@ LDAP-timeout coverage (Security-009) plus extra no-service-account / DN-path edg
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/LdapAuthService.cs:78-118` |
|
||||
|
||||
**Description**
|
||||
@@ -515,7 +515,19 @@ or be surfaced.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
Resolved 2026-05-17 (commit `pending`). Confirmed: the group/attribute `Search` was
|
||||
wrapped in a `catch (LdapException)` that logged a warning and returned
|
||||
`new LdapAuthResult(true, …, groups: [])` — a partial LDAP outage produced an
|
||||
authenticated, zero-role session — and the inner `while`-loop `catch { break; }` masked
|
||||
real mid-enumeration errors as end-of-results. `AuthenticateAsync` now tracks a
|
||||
`groupLookupSucceeded` flag (false when any `LdapException` is thrown by the search or
|
||||
`Next()`); the inner swallowing catch was removed so such errors propagate; the new
|
||||
`BuildAuthResultFromGroupLookup` helper returns a FAILED `LdapAuthResult`
|
||||
("directory temporarily unavailable, please try again") on lookup failure while still
|
||||
treating a genuine empty-groups result as a successful login. Regression tests
|
||||
`BuildAuthResultFromGroupLookup_LookupFailed_FailsTheLogin`,
|
||||
`BuildAuthResultFromGroupLookup_LookupSucceededNoGroups_IsAuthenticated`,
|
||||
`BuildAuthResultFromGroupLookup_LookupSucceededWithGroups_CarriesGroups`.
|
||||
|
||||
### Security-013 — `ExtractFirstRdnValue` mis-parses group DNs containing escaped commas
|
||||
|
||||
@@ -523,7 +535,7 @@ _Unresolved._
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/LdapAuthService.cs:258-269` |
|
||||
|
||||
**Description**
|
||||
@@ -545,7 +557,17 @@ library's DN-parsing API (`LdapDN` / RDN accessors) rather than hand-rolled `Ind
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
Resolved 2026-05-17 (commit `pending`). Confirmed: `ExtractFirstRdnValue` took the
|
||||
substring between the first `=` and the first `,`, splitting on an RFC 4514
|
||||
backslash-escaped comma so a CN like `Acme\, Inc Operators` became `Acme\`. Replaced the
|
||||
naive `IndexOf` scan with an escape-aware character scan that ignores a backslash-escaped
|
||||
`,`, unescapes single-character and `\XX` hex escape sequences, and only terminates the
|
||||
value on an unescaped comma — so a comma-containing group CN is returned intact and
|
||||
matches its configured `LdapGroupName`. Method was also made `public` for direct
|
||||
testing. Regression tests `ExtractFirstRdnValue_EscapedComma_KeepsWholeGroupName`,
|
||||
`ExtractFirstRdnValue_PlainDn_ReturnsFirstRdnValue`,
|
||||
`ExtractFirstRdnValue_SingleRdn_ReturnsValue`,
|
||||
`ExtractFirstRdnValue_EscapedSpecials_AreUnescaped`.
|
||||
|
||||
### Security-014 — `RefreshToken` re-issues a token without checking the idle timeout
|
||||
|
||||
@@ -553,7 +575,7 @@ _Unresolved._
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/JwtTokenService.cs:156-169` |
|
||||
|
||||
**Description**
|
||||
@@ -580,7 +602,16 @@ regression test covering refresh of a 31-minute-idle token.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
Resolved 2026-05-17 (commit `pending`). Confirmed: `RefreshToken` carried `LastActivity`
|
||||
forward and minted a fresh 15-minute token without ever calling `IsIdleTimedOut`, so the
|
||||
idle policy depended entirely on caller discipline and a background refresh could keep an
|
||||
idle-expired session alive indefinitely. `RefreshToken` now calls
|
||||
`IsIdleTimedOut(currentPrincipal)` after the missing-claims check and returns `null`
|
||||
(its existing "cannot refresh" signal) when the session is past the idle window, so the
|
||||
30-minute idle policy holds regardless of caller order; XML doc updated to state the
|
||||
invariant. Regression tests `RefreshToken_IdleExpiredPrincipal_ReturnsNull`,
|
||||
`RefreshToken_ActiveSessionWithinIdleWindow_StillRefreshes`,
|
||||
`RefreshToken_MissingLastActivityClaim_ReturnsNull`.
|
||||
|
||||
### Security-015 — Username is not trimmed before use in the LDAP filter, fallback DN, and JWT claims
|
||||
|
||||
@@ -588,7 +619,7 @@ _Unresolved._
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/LdapAuthService.cs:20-21`, `:80`, `:122`, `:169`, `:193` |
|
||||
|
||||
**Description**
|
||||
@@ -613,4 +644,13 @@ trail.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
Resolved 2026-05-17 (commit `pending`). Confirmed: an otherwise-valid username with
|
||||
leading/trailing whitespace passed the `IsNullOrWhiteSpace` guard and flowed verbatim
|
||||
into the LDAP filter, the fallback DN, and the returned `LdapAuthResult` (and thus the
|
||||
JWT `Username` claim). Added a `NormalizeUsername` helper (trims whitespace) called once
|
||||
at the top of `AuthenticateAsync` immediately after the guard; the local `username`
|
||||
variable is reassigned to the trimmed value so the filter, fallback DN, and result all
|
||||
use the single canonical identity. Regression tests
|
||||
`NormalizeUsername_TrimsLeadingAndTrailingWhitespace`,
|
||||
`BuildFallbackUserDn_TrimmedUsername_NoLeadingTrailingSpace`,
|
||||
`AuthenticateAsync_UsernameWithSurroundingWhitespace_StillRejectedForInsecure`.
|
||||
|
||||
Reference in New Issue
Block a user