Repository navigation
Commit 8f20d76
authored
* sec(auth): verifyTOTP and consumeRecoveryCode fail closed on empty input (#1018)
verifyTOTP("","") previously returned true because generateTOTP("")
returns "" on base32-decode failure, and ConstantTimeCompare("","") == 1.
This created an MFA-bypass primitive callable by any session holder via
MFARegenerateRecoveryCodes with code="".
- verifyTOTP: early-return false when code=="" or secret==""
- consumeRecoveryCode: early-return false when normalized input is empty
- Fix misleading comment at verifyPasswordAndMFA (service.go:212) that
described the old panic-safe behaviour; the function now fails closed
Closes #1018
* fix(auth): bound API key last-used updates via singleflight (#1027)
ValidateUserAPIKey fired one unbounded goroutine per request to update
the last-used timestamp. Under a burst of authenticated requests (or
a revoked key being presented repeatedly), this spawned an unbounded
number of goroutines writing to the DB with context.Background() and
logged a Warn for every "key not found" event.
- Replace raw go func() with singleflight.Group.Do: at most one DB write
per key is in-flight at any moment; concurrent requests for the same
key are coalesced rather than each spawning a writer
- Demote "failed to update last-used" log from Warn to Debug
Closes #1027
* fix(credentials): use errors.Is for pgx.ErrNoRows sentinel in LoadRaw (#1028)
store.go:LoadRaw used direct pointer equality (err == pgx.ErrNoRows).
Pool and transaction wrappers wrap the sentinel, so the check returned
false and LoadRaw propagated a hard error instead of the documented
(nil, nil) "not found" contract, breaking callers like resolveAccessKeyProvider.
- Change to errors.Is(err, pgx.ErrNoRows) to unwrap the error chain
- Add errors import
Closes #1028
* sec(auth): derive CSRF token as HMAC-SHA256 of session token, not random (#1029)
The CSRF token was a random hex string stored plaintext in the sessions
row alongside the hashed session token. A DB read (SQLi, backup, replica)
yielded a directly usable CSRF token, undermining the double-submit
protection.
New approach (no DB migration needed):
- createSession derives CSRF as HMAC-SHA256(csrfKey, rawSessionToken)
- The MAC is written to csrf_token for diagnostics/observability but is
never used for validation
- ValidateCSRFToken recomputes the HMAC from the raw session token and
compares with the presented token; the stored column is not read
- csrfKey added to Service struct and ServiceConfig; if not supplied,
NewService generates a random ephemeral key and logs a warning
The token is now cryptographically bound to the session token AND the
server-side key. A DB read alone cannot produce a forgeable CSRF token.
Closes #1029
* test(auth,credentials): add regression tests for #1018, #1027, #1028, #1029
- TestVerifyTOTP_FailClosed: verifyTOTP("","") must return false (was true)
- TestConsumeRecoveryCode_EmptyInput: empty code must be rejected before bcrypt
- TestMFARegenerateRecoveryCodes_EmptyCode: empty code must return error without
calling UpdateUser
- TestCSRFToken_DerivedFromSessionToken: CSRF equals HMAC(csrfKey, rawToken);
ValidateCSRFToken accepts derived token, rejects cleartext legacy tokens
- TestCSRFToken_CrossSessionRejected: CSRF token from session A is rejected for B
- TestValidateUserAPIKey_LastUsedSingleflight: background last-used update runs;
done channel replaces time.Sleep per feedback_no_sleep_in_tests
- TestLoadRaw_ErrorsIs_WrappedNoRows: errors.Is unwraps a wrapped pgx.ErrNoRows
where == would return false (the pre-fix bug)
* fix(credentials): explicit 32-byte key guard + hard error on multi-key config
03-M1: add `len(key) != 32` guard at the top of Encrypt and Decrypt so a
16- or 24-byte key returns a clear error rather than silently selecting
AES-128/192. AES spec accepts shorter keys without error; the guard is
needed because callers can supply a key directly, bypassing LoadKey.
03-M2: treat multiple key-source env vars simultaneously set as a hard
startup error (ErrMultipleKeys) instead of a WARN + first-wins. Ambiguous
key selection on a data-encryption key is a misconfiguration that must
not proceed silently, especially given the rekey workflow.
Regression tests updated/extended for both changes.
Closes #1058 (partial)
* fix(secrets): GCP ListSecrets uses client-side HasPrefix, not API filter expression
03-M3: The Resolver interface documents GCP as using strings.HasPrefix
filtering, but GCPResolver.ListSecrets was forwarding the caller's filter
directly as a GCP expression-language Filter field, making the three
implementations (Azure/env use HasPrefix; GCP used expression-language;
AWS does API substring) inconsistent.
Fix: remove the Filter from the ListSecretsRequest; apply strings.HasPrefix
on the short name (path.Base) client-side so all implementations honour the
documented contract. Updated regression tests confirm the API request carries
no Filter expression.
Closes #1058 (partial)
* sec(oidc): validate Azure exponent range and precise half-configured error
03-M6: guard `new(big.Int).SetBytes(E).Int64()` in AzureKeyVaultSigner.resolveOnce
with an IsInt64 + range check before the int() conversion. An oversized or
non-positive exponent now returns an explicit error instead of silently
corrupting the rsa.PublicKey and producing an unusable JWKS.
03-M7: NewSignerFromEnv Azure branch now emits a precise error when exactly
one of CUDLY_SIGNING_KEY_VAULT_URL and CUDLY_SIGNING_KEY_NAME is set, naming
the missing variable, instead of deferring to the constructor's generic error.
Regression tests added for both cases.
Closes #1058 (partial)
* fix(auth): rate-limit password reset requests per email
03-M5: RequestPasswordReset now silently no-ops when a reset token was
issued within PasswordResetRateLimit (1 minute). An adversary who knows
a victim's email could previously call the endpoint repeatedly to
perpetually invalidate the victim's legitimate reset link.
Detection is via the existing PasswordResetExpiry field (no schema change):
if the token's age is less than PasswordResetRateLimit, the function returns
nil without overwriting the token or sending a new email. The existing
token remains valid. The response is indistinguishable from a successful
request to preserve email-enumeration protection.
Regression test: TestService_RequestPasswordReset_RateLimit.
Closes #1058 (partial)
* fix(auth,oidc): L1-L6 low-severity hardening in auth and OIDC subsystems
03-L1: redactEmail now masks both local prefix and domain hostname (was
"us***@example.com", now "us***@ex***.com") to reduce PII exposure for
low-entropy addresses in debug logs.
03-L2: apiKey[:8] slice in CreateAPIKey guarded with len check so a future
change in key-generation length cannot panic.
03-L3: containsSequentialChars renamed to containsRepeatedChars and
sequentialCharThreshold to repeatedCharThreshold; the function counts
identical consecutive chars, not sequential runs -- the "sequential"
naming was misleading.
03-L4: SetIssuerURL now validates that the URL is an absolute https://
URL and returns an error for non-https, relative, or unparseable URLs.
Callers updated (lambda_issuer, handler_oidc, server/app).
03-L5: IsUnrestrictedAccess doc extended with a prominent fail-open
WARNING; empty allowed-list = unrestricted is documented as intentional
backward-compat but flagged so new callers cannot accidentally rely on it
for "deny all" semantics.
03-L6: matchStringListConstraints doc extended to explain the empty-list
semantics (empty either side = satisfied) and the authz rationale.
Closes #1058 (partial)
* fix(auth,credentials): N4 drop pointless SHA-256 in token gen; N5 validate unmarshal fields
03-N4: generateToken now encodes 32 CSPRNG bytes directly as base64url
instead of hashing them through SHA-256. Hashing CSPRNG output adds no
entropy -- the raw bytes already carry 256 bits. The SHA-256 step could
mislead readers into thinking the input was sensitive pre-image material.
03-N5: resolveAccessKeyProvider and ResolveAzureCredentials now validate
that required payload fields (access_key_id, secret_access_key, client_secret)
are non-empty after JSON unmarshal. Previously an empty field would flow
through to the AWS SDK or Azure SDK and fail with a less descriptive error
deep in the credential chain.
Regression tests added for both N5 paths.
Closes #1058 (partial)
1 parent ee8212e commit 8f20d76
28 files changed
Lines changed: 977 additions & 134 deletions
File tree
- internal
- api
- auth
- credentials
- oidc
- secrets
- server
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
| |||
124 | 125 | | |
125 | 126 | | |
126 | 127 | | |
127 | | - | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
128 | 135 | | |
129 | 136 | | |
130 | 137 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| 13 | + | |
12 | 14 | | |
13 | 15 | | |
14 | 16 | | |
| |||
41 | 43 | | |
42 | 44 | | |
43 | 45 | | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
44 | 55 | | |
45 | 56 | | |
46 | 57 | | |
| |||
50 | 61 | | |
51 | 62 | | |
52 | 63 | | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
53 | 69 | | |
54 | 70 | | |
55 | 71 | | |
| |||
83 | 99 | | |
84 | 100 | | |
85 | 101 | | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
86 | 115 | | |
87 | 116 | | |
88 | 117 | | |
89 | 118 | | |
90 | 119 | | |
91 | 120 | | |
| 121 | + | |
92 | 122 | | |
93 | 123 | | |
94 | 124 | | |
| |||
209 | 239 | | |
210 | 240 | | |
211 | 241 | | |
212 | | - | |
213 | | - | |
| 242 | + | |
| 243 | + | |
214 | 244 | | |
215 | 245 | | |
216 | 246 | | |
| |||
312 | 342 | | |
313 | 343 | | |
314 | 344 | | |
315 | | - | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
316 | 351 | | |
317 | 352 | | |
318 | 353 | | |
319 | 354 | | |
320 | 355 | | |
321 | | - | |
322 | | - | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
323 | 360 | | |
324 | 361 | | |
325 | 362 | | |
326 | | - | |
327 | | - | |
328 | | - | |
329 | | - | |
330 | | - | |
331 | | - | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
332 | 366 | | |
333 | | - | |
334 | | - | |
| 367 | + | |
| 368 | + | |
335 | 369 | | |
336 | 370 | | |
337 | 371 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | | - | |
54 | | - | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
55 | 60 | | |
56 | 61 | | |
57 | 62 | | |
| |||
269 | 274 | | |
270 | 275 | | |
271 | 276 | | |
272 | | - | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
273 | 283 | | |
274 | | - | |
275 | | - | |
276 | | - | |
277 | | - | |
278 | | - | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
279 | 292 | | |
280 | 293 | | |
281 | 294 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
229 | 229 | | |
230 | 230 | | |
231 | 231 | | |
232 | | - | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
233 | 245 | | |
234 | 246 | | |
235 | 247 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
| 8 | + | |
7 | 9 | | |
8 | 10 | | |
9 | 11 | | |
| |||
16 | 18 | | |
17 | 19 | | |
18 | 20 | | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
19 | 31 | | |
20 | 32 | | |
21 | 33 | | |
| |||
24 | 36 | | |
25 | 37 | | |
26 | 38 | | |
27 | | - | |
28 | | - | |
| 39 | + | |
| 40 | + | |
29 | 41 | | |
30 | 42 | | |
31 | | - | |
32 | | - | |
33 | | - | |
34 | | - | |
35 | | - | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
36 | 48 | | |
37 | | - | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
38 | 53 | | |
39 | 54 | | |
40 | 55 | | |
| |||
43 | 58 | | |
44 | 59 | | |
45 | 60 | | |
46 | | - | |
| 61 | + | |
47 | 62 | | |
48 | 63 | | |
49 | 64 | | |
| |||
59 | 74 | | |
60 | 75 | | |
61 | 76 | | |
62 | | - | |
| 77 | + | |
63 | 78 | | |
64 | 79 | | |
65 | 80 | | |
66 | 81 | | |
67 | 82 | | |
68 | | - | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
69 | 86 | | |
70 | | - | |
71 | | - | |
| 87 | + | |
| 88 | + | |
72 | 89 | | |
73 | 90 | | |
74 | | - | |
75 | | - | |
| 91 | + | |
76 | 92 | | |
77 | 93 | | |
78 | 94 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
49 | | - | |
50 | | - | |
51 | | - | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
52 | 58 | | |
53 | | - | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
54 | 70 | | |
55 | 71 | | |
56 | 72 | | |
57 | | - | |
58 | | - | |
| 73 | + | |
| 74 | + | |
59 | 75 | | |
60 | 76 | | |
61 | 77 | | |
62 | 78 | | |
63 | | - | |
| 79 | + | |
| 80 | + | |
64 | 81 | | |
65 | 82 | | |
66 | 83 | | |
| |||
207 | 224 | | |
208 | 225 | | |
209 | 226 | | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
210 | 231 | | |
211 | 232 | | |
212 | 233 | | |
213 | 234 | | |
214 | 235 | | |
215 | 236 | | |
216 | 237 | | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
217 | 242 | | |
218 | 243 | | |
219 | 244 | | |
| |||
0 commit comments