Commit 664cf42
feat(limits): production-readiness PR 2 — resource limits & abuse protection (#107)
Second of 5 production-readiness PRs (stacked on #106). Closes the resource-
exhaustion and abuse vectors that PR 1 (auth) intentionally deferred.
Why
---
The serve surface is exposed to authenticated clients but has no per-tool
guardrails: an MCP client can issue an unbounded `run_cypher`, ask for a
`trace_impact` depth of 1000, hammer the rate-limit-free endpoints, or get
served binary content as text/plain. Each is its own DoS or readability hazard.
Changes
-------
* **Cypher transaction timeout** — `Neo4jConfig` sets DBMS-level
`transaction_timeout=30s` so any pathological Cypher (cartesian explosion,
forgotten LIMIT) is killed by the DB regardless of client.
* **`run_cypher` row cap** — MCP `run_cypher` truncates at `mcp.limits.max_results`
rows and adds a `truncated: true` flag in the response, so clients see the
cap explicitly.
* **MCP `trace_impact` depth cap** — clamped to `mcp.limits.max_depth` (default
10). New config field on `McpLimitsConfig`; YAML accepts `max_depth` /
`maxDepth` (deprecated alias).
* **Cached stats snapshot** — `getCachedData()` swapped from a manual map to an
`AtomicReference<CachedSnapshot>` with 60s TTL. Avoids OOM from the previous
unbounded weak-keyed accumulator under spiky workloads.
* **Per-client rate limiter** — new `RateLimitFilter` using Bucket4j 8.18.0
(`bucket4j_jdk17-core`, Apache-2.0). 300 req/min default, configurable via
`mcp.limits.rate_per_minute`. Client key is `SHA-256(Authorization-header)`,
with `X-Forwarded-For` fallback for unauthenticated probes. Returns 429 with
`Retry-After` and `X-RateLimit-Remaining` headers. Permits health/static.
* **`/api/file` content-type guard** — `Files.probeContentType()` returns 415
for non-text MIMEs (.jks, .png, .so, etc.). Stops slow-client tarpit on
binary downloads holding virtual threads + Tomcat connections for ~1000s.
* **Tomcat slow-client tarpit caps** — `connection-timeout=10000`,
`max-swallow-size=1MB` so a stalled client can't pin a thread indefinitely.
* **CodeQL hardening** —
- `BearerAuthFilter`: new `sanitizeForLog()` strips control chars from
request method/URI before they hit the rejection log (java/log-injection
/ CWE-117). Capped at 256 chars to defend against giant URI log bombs.
- `TokenResolver`: dropped env-var-name from log message (operator config
can be tainted; java/sensitive-log).
- `SecurityConfig`: documented CSRF disable rationale inline (bearer-only
stateless model — see prose comment for why this is safe; CSRF doesn't
apply when no Set-Cookie is issued).
Test coverage
-------------
* New `RateLimitFilterTest` — 10 cases: bucket consumption, 429 + Retry-After,
separate buckets per client, header hashing, X-Forwarded-For precedence,
permit-list bypass, default fallback when no auth/XFF.
* `McpToolsTest` / `McpToolsExpandedTest` / `McpToolsEvidenceTest` /
`TopologyEndpointTest` updated for new `McpTools` constructor signature
(added `CodeIqUnifiedConfig` param for limit lookup).
* `TokenResolverTest` updated for new 5-arg `McpLimitsConfig`.
* Full suite: 3672 tests, 0 failures, 0 errors, 32 skipped (pre-existing).
Refs: docs/audits/2026-04-28-serve-path-prod-readiness*.md (audit findings)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 04ceaf1 commit 664cf42
22 files changed
Lines changed: 570 additions & 27 deletions
File tree
- src
- main
- java/io/github/randomcodespace/iq
- api
- config
- security
- unified
- mcp
- resources
- test/java/io/github/randomcodespace/iq
- api
- config/security
- mcp
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
305 | 305 | | |
306 | 306 | | |
307 | 307 | | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
308 | 368 | | |
309 | 369 | | |
310 | 370 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
439 | 439 | | |
440 | 440 | | |
441 | 441 | | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
442 | 450 | | |
443 | 451 | | |
444 | 452 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
160 | 160 | | |
161 | 161 | | |
162 | 162 | | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
163 | 173 | | |
164 | 174 | | |
165 | 175 | | |
| |||
Lines changed: 20 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
298 | 298 | | |
299 | 299 | | |
300 | 300 | | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
301 | 321 | | |
302 | 322 | | |
303 | 323 | | |
| |||
Lines changed: 7 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| 20 | + | |
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
| |||
40 | 41 | | |
41 | 42 | | |
42 | 43 | | |
43 | | - | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
44 | 50 | | |
45 | 51 | | |
46 | 52 | | |
| |||
Lines changed: 7 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
88 | 88 | | |
89 | 89 | | |
90 | 90 | | |
91 | | - | |
| 91 | + | |
| 92 | + | |
92 | 93 | | |
93 | 94 | | |
94 | 95 | | |
| |||
150 | 151 | | |
151 | 152 | | |
152 | 153 | | |
153 | | - | |
154 | | - | |
155 | | - | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
156 | 159 | | |
157 | 160 | | |
158 | 161 | | |
| |||
0 commit comments