fix(security): resolve Security-001/002/003 — reachable StartTLS path, Secure cookie, JWT signing key validation

This commit is contained in:
Joseph Doherty
2026-05-16 19:47:17 -04:00
parent 393172f169
commit 0d9363766d
7 changed files with 222 additions and 11 deletions

View File

@@ -8,7 +8,7 @@
| Last reviewed | 2026-05-16 |
| Reviewer | claude-agent |
| Commit reviewed | `9c60592` |
| Open findings | 11 |
| Open findings | 8 |
## Summary
@@ -51,7 +51,7 @@ defects that should be fixed before any production deployment.
|--|--|
| Severity | High |
| Category | Security |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.Security/LdapAuthService.cs:37-47` |
**Description**
@@ -74,7 +74,12 @@ session is encrypted before binding. Remove the unreachable conditional.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit `<pending>`). Added an explicit `LdapTransport` enum
(`Ldaps`/`StartTls`/`None`); `SecureSocketLayer` is set only for LDAPS, and the
StartTLS branch now connects in plaintext, calls `StartTls()`, and verifies
`connection.Tls` before binding. `LdapUseTls` is retained as a compatibility shim
mapping onto the enum. Regression tests `AuthenticateAsync_StartTlsTransport_AttemptsConnection`
and `AuthenticateAsync_NoTlsTransport_RejectedWithoutAllowInsecure`.
### Security-002 — Authentication cookie is not marked `Secure`
@@ -82,7 +87,7 @@ _Unresolved._
|--|--|
| Severity | High |
| Category | Security |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.Security/ServiceCollectionExtensions.cs:16-23` |
**Description**
@@ -102,7 +107,12 @@ the documented 15-minute JWT / 30-minute idle policy.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit `<pending>`). Confirmed the cookie is configured in this
module (`ServiceCollectionExtensions.AddSecurity`), not CentralUI — no misattribution.
Added `options.Cookie.SecurePolicy = CookieSecurePolicy.Always` so the JWT-bearing
cookie is never sent over plain HTTP. Regression test
`AddSecurity_AuthCookie_IsMarkedSecureAlways`. (`ExpireTimeSpan`/`SlidingExpiration`
tuning left as a separate, lower-priority improvement.)
### Security-003 — JWT signing key length is never validated
@@ -110,7 +120,7 @@ _Unresolved._
|--|--|
| Severity | High |
| Category | Security |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.Security/JwtTokenService.cs:33`, `src/ScadaLink.Security/SecurityOptions.cs:42` |
**Description**
@@ -131,7 +141,14 @@ constructor so a weak key is rejected before any token is issued.
**Resolution**
_Unresolved._
Resolved 2026-05-16 (commit `<pending>`). The `JwtTokenService` constructor now
fails fast with an `InvalidOperationException` when `JwtSigningKey` is empty or
shorter than 32 bytes (`SecurityOptions.MinJwtSigningKeyBytes`), so a weak key is
rejected before any token is issued. The misleading `SecurityOptions` XML doc was
corrected to state the requirement. Regression tests
`JwtTokenService_EmptySigningKey_ThrowsAtConstruction`,
`JwtTokenService_ShortSigningKey_ThrowsAtConstruction`, and
`JwtTokenService_AdequateSigningKey_ConstructsSuccessfully`.
### Security-004 — Search filter uses `uid=` while fallback DN construction uses `cn=`