Repository navigation
Commit c85bb9c
authored
* sec(api): add "register" rate-limit bucket and fix CSRF prefix match
Issue #1016: "register" was missing from getDefaultRateLimits(), causing
checkRateLimit() to silently fall through to api_general (300/min). The
public /api/register endpoint writes to the DB and triggers a synchronous
admin-notification email per request, making 300/min a viable
table-flooding and inbox-spam vector. Add a dedicated "register" bucket
matching setup_admin severity (5 per 15 min per IP).
Add assertRateLimitKeysKnown(), called from NewHandler with every key used
in checkRateLimit call sites. Panics at startup if a key is missing from
the config map, catching this class of bug before production traffic hits.
Issue #1017: csrfExemptAlways used strings.HasPrefix on "/api/register",
which also exempted /api/registrations/<uuid>/approve, /reject, and DELETE
(all AuthAdmin, state-changing). Approving a registration creates an
enabled cloud account and stores credentials. Split the exempt list into a
prefix list for /api/auth/* paths and an exact match for /api/register,
mirroring the existing isPublicEndpoint() handling.
Regression tests:
- TestGetDefaultRateLimits_RegisterHasDedicatedBucket
- TestAssertRateLimitKeysKnown_PanicsOnUnknownKey
- TestAssertRateLimitKeysKnown_NoopForKnownKeys
- TestRequiresCSRFValidation_RegistrationsAdminRoutesAreNotExempt
Closes #1016, Closes #1017
* test(api): assert exact register rate-limit contract (5/15min)
Lock the register regression test to the precise security contract from
issue #1016 instead of only checking "stricter than api_general" and a
non-zero window. A weak value like 299/min would have passed the old
assertions while still allowing registration flooding. Assert the exact
5 attempts / 15 minutes contract so any future relaxation is caught.
Addresses CodeRabbit nitpick on PR #1038.
* fix(api): fix stale forgot-password comment and health info-leak (02-L1, 02-L4)
02-L1: update the forgot_password handler comment from
"3 attempts per email per hour" to match the actual config
(10 attempts / 5 minutes in getDefaultRateLimits).
02-L4: health.go checkConfigStore exposed err.Error() in the public
/health response body (AuthPublic route). Log the detail server-side
only and return a static "config store unavailable" message to callers.
Update tests to assert the error string does not appear in the body.
Closes #1064
* refactor(api): remove dead RateLimiter type and type-safe toAPIPermissions (02-N1, 02-N2)
02-N1: remove legacy RateLimiter/newRateLimiter/rateLimitEntry from
types.go and rate_limiter.go. Production paths wire InMemoryRateLimiter
or DBRateLimiter (both implement RateLimiterInterface). The dead code
carried the same unbounded-GC bug as 02-M3 and was a maintenance trap.
Remove matching tests; redirect rate_limiter_test.go to a comment noting
where the config-function tests now live.
02-N2: change toAPIPermissions from []any (silent-drop type-assertion) to
[]Permission (typed, total). Replace the apologetic "in production this would
use proper conversion" comment with a clear copy-contract doc. Update tests:
replace the now-impossible NonPermissionItems test with a ReturnsCopy test
that asserts the mutation-isolation contract.
Closes #1064
* fix(api): cap in-memory rate limiter entries with oldest-reset eviction (02-M3)
The previous GC only removed already-expired entries above 1000 keys,
allowing an attacker rotating source IPs to grow the map without bound
(expired entries are harmless, so an adversary is never pruned). The
fix lowers the hard cap to 500, evicts all expired entries first, then
evicts the oldest live entries (by resetTime) down to 80% of cap if the
map is still at or above the cap.
Add a regression test (TestInMemoryRateLimiter_EntriesCapEnforced) that
injects cap+2 unique IPs, confirms that one eviction cycle fires and the
final map is strictly below cap. Previously this test would reach cap+2.
Closes #1064
* fix(api): add scheduled DB rate-limiter cleanup worker (02-M2)
The existing opportunistic cleanup path (triggered when count==1) never
fires for perpetually-denied keys: under sustained abuse the abusing key's
count keeps climbing without ever resetting to 1, so its row escapes the
24-hour eviction window indefinitely.
Add StartCleanupWorker(ctx) which launches a background goroutine that
runs cleanup() on a fixed schedule (default 10 minutes), independent of
traffic. Wire it in server/app.go immediately after NewDBRateLimiter so
Lambda deployments benefit on startup.
The integer width of count is Go's default int (64-bit on all supported
platforms), so unbounded growth is cosmetically undesirable but not an
overflow risk.
Add TestDBRateLimiter_StartCleanupWorker_StopsOnContextCancel: overrides
the ticker interval to 5ms, confirms the worker exits cleanly on cancel.
Closes #1064
* sec(api): fail-closed rate limiting on credential endpoints (02-M1)
The global checkRateLimit swallows limiter errors and allows requests
through (fail-open). For data/general endpoints this is intentional, but
for the credential brute-force surface (login, setup_admin, reset_password,
change_password) it means any DB pressure that causes the rate limiter to
error silently disables protection. An attacker who can induce DB pressure
also unlocks unlimited password guessing.
Add checkRateLimitStrict (fail-closed): returns 503 on limiter error with
a high-severity ALERT log so monitoring can detect and alert on the
fail-closed window. Switch login, setup_admin, reset_password (both sites),
and change_password to checkRateLimitStrict.
For forgot_password (email-keyed, inline call): emit an ALERT log on error
but keep fail-open because failing closed here would return 503 vs 200,
leaking infrastructure state to an enumerating attacker; the enumeration
risk is the higher concern.
For createAPIKey (authenticated, user-keyed): upgrade the warn log to an
error ALERT so the fail-open window is observable, but keep fail-open
because the endpoint is behind requirePermission.
Add three regression tests:
- TestHandler_checkRateLimitStrict_FailsClosedOnError: 503 on db error
- TestHandler_checkRateLimit_FailsOpenOnError: nil (allow) on db error
- TestHandler_checkRateLimitStrict_Returns429WhenDenied: normal deny path
Closes #1064
* refactor(api): extract eviction helpers to fix gocyclo on Allow (CI)
Allow in InMemoryRateLimiter had cyclomatic complexity 13, over the
pre-commit gocyclo threshold of 10. Extract two helpers:
- evictIfAtCap: cap-guard + expired-entry purge
- evictOldest: oldest-first eviction down to 80% of cap
Allow drops to CC=5; helpers are CC=5 and CC=4. No logic change.
Also fix gofmt on rate_limiter.go (trailing blank line) and
pkg/exchange/reshape_crossfamily_test.go (alignment in struct literals).
1 parent 278a467 commit c85bb9c
17 files changed
Lines changed: 434 additions & 209 deletions
File tree
- internal
- api
- server
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
32 | 40 | | |
33 | 41 | | |
34 | 42 | | |
35 | 43 | | |
36 | | - | |
| 44 | + | |
37 | 45 | | |
38 | 46 | | |
39 | 47 | | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
40 | 73 | | |
41 | 74 | | |
42 | 75 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
108 | 108 | | |
109 | 109 | | |
110 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
180 | 180 | | |
181 | 181 | | |
182 | 182 | | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
183 | 197 | | |
184 | 198 | | |
185 | 199 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | | - | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
46 | 49 | | |
47 | 50 | | |
48 | 51 | | |
49 | | - | |
| 52 | + | |
| 53 | + | |
50 | 54 | | |
51 | 55 | | |
52 | 56 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
224 | 224 | | |
225 | 225 | | |
226 | 226 | | |
227 | | - | |
| 227 | + | |
228 | 228 | | |
229 | 229 | | |
230 | 230 | | |
| |||
253 | 253 | | |
254 | 254 | | |
255 | 255 | | |
256 | | - | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
257 | 260 | | |
258 | 261 | | |
259 | 262 | | |
260 | | - | |
261 | | - | |
| 263 | + | |
| 264 | + | |
262 | 265 | | |
263 | 266 | | |
264 | 267 | | |
| |||
303 | 306 | | |
304 | 307 | | |
305 | 308 | | |
306 | | - | |
| 309 | + | |
307 | 310 | | |
308 | 311 | | |
309 | 312 | | |
| |||
332 | 335 | | |
333 | 336 | | |
334 | 337 | | |
335 | | - | |
| 338 | + | |
336 | 339 | | |
337 | 340 | | |
338 | 341 | | |
| |||
465 | 468 | | |
466 | 469 | | |
467 | 470 | | |
468 | | - | |
| 471 | + | |
469 | 472 | | |
470 | 473 | | |
471 | 474 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
776 | 776 | | |
777 | 777 | | |
778 | 778 | | |
779 | | - | |
| 779 | + | |
780 | 780 | | |
781 | 781 | | |
782 | | - | |
783 | | - | |
784 | | - | |
785 | | - | |
| 782 | + | |
786 | 783 | | |
787 | 784 | | |
788 | 785 | | |
789 | 786 | | |
790 | | - | |
791 | | - | |
792 | | - | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
793 | 790 | | |
794 | 791 | | |
795 | 792 | | |
| |||
799 | 796 | | |
800 | 797 | | |
801 | 798 | | |
802 | | - | |
803 | | - | |
804 | | - | |
805 | | - | |
806 | | - | |
807 | | - | |
808 | | - | |
809 | | - | |
810 | | - | |
811 | | - | |
812 | | - | |
813 | | - | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
814 | 805 | | |
815 | 806 | | |
816 | 807 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
64 | 65 | | |
65 | 66 | | |
66 | 67 | | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
67 | 71 | | |
68 | 72 | | |
69 | | - | |
| 73 | + | |
70 | 74 | | |
71 | 75 | | |
72 | 76 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
85 | 85 | | |
86 | 86 | | |
87 | 87 | | |
88 | | - | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
89 | 91 | | |
90 | 92 | | |
91 | 93 | | |
| |||
179 | 181 | | |
180 | 182 | | |
181 | 183 | | |
182 | | - | |
183 | | - | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
184 | 187 | | |
185 | 188 | | |
186 | 189 | | |
| |||
0 commit comments