Harden MCP server: auth, CORS, Redis, widgets, and review fixes#196
Harden MCP server: auth, CORS, Redis, widgets, and review fixes#196
Conversation
|
@claude code review |
|
Claude finished @RafaelPo's task in 2m 38s —— View job Code Review SummaryReviewed all 100 changed files across the auth, Redis, HTTP transport, middleware, routes, tools, models, config, templates, deployment, and test modules. Overall this is a well-structured PR with solid security hardening. The auth flow, rate limiting, and Redis-backed state management are implemented carefully. Below are the issues I found, grouped by severity. Issues1. Refresh token GET-then-DELETE race condition (Medium)
While Supabase will invalidate the old refresh token on first use (making the second refresh fail), the losing request would then hit the error path and re-store the consumed token (line 507-512), potentially creating a stale refresh token in Redis. Consider using async def load_refresh_token(
self, client: OAuthClientInformationFull, refresh_token: str
) -> EveryRowRefreshToken | None:
if len(refresh_token) > 256:
return None
key = build_key("refresh", refresh_token)
data = await self._redis.getdel(key) # Atomic consume
if data is None:
return None
rt = EveryRowRefreshToken.model_validate_json(data)
if rt.client_id != client.client_id:
# Re-store for the legitimate client
remaining = settings.refresh_token_ttl
await self._redis.setex(key, remaining, data)
return None
return rt2. CSV cache size check uses character count, not byte count (Low)
# Option A: rename the constant
MAX_CSV_CACHE_CHARS = 50 * 1024 * 1024
# Option B: check actual byte size
if len(csv_text.encode("utf-8")) > MAX_CSV_CACHE_BYTES:3. Accessing private attribute across class boundary (Low)
# Consider exposing as a property on SupabaseTokenVerifier:
@property
def revocation_ttl(self) -> int:
return self._revocation_ttl4. f-string in logger call (Nit)
logging.getLogger(__name__).error(f"everyrow-mcp startup failed: {e!r}")Uses an f-string instead of lazy logging.getLogger(__name__).error("everyrow-mcp startup failed: %r", e)5. Dockerfile missing HEALTHCHECK (Nit)
The HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"What looks good
|
Security & auth hardening: - Auth code expiration defense-in-depth check after deserialization - Atomic auth code consumption via GETDEL (re-store on client mismatch) - Revocation TTL derived from token remaining lifetime + 60s buffer - SameSite=strict on auth state cookies - Supabase response validation via Pydantic model_validate - Remove exc_info=True from JWT failure debug log Infrastructure improvements: - CORS: restrict origin fallback to localhost (was "*"), add Allow-Headers - Redis: replace @lru_cache singleton with explicit getter/setter for test safety - Prefix-sum optimization for page budget clamping (was O(n log n) JSON serialization) - Exception chaining in UUID validation (raise from exc) - Add missing esc() XSS helper to session widget - Add MCP_SERVER_URL to .env.example - Add .dockerignore, Sentinel support in docker-compose - Add real Redis integration tests for HTTP result flow - Rate limiter: handle None client IP, proxy header trust gating Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8d8ab6d to
b06d137
Compare
|
@claude code review |
|
Claude finished @RafaelPo's task in 3m 43s —— View job Code Review: Harden MCP server
Overall this is a solid hardening PR. The changes are well-structured, defensive, and thoroughly tested. I have a few suggestions below but nothing blocking. Findings1. Auth code re-store on client mismatch — nice pattern (auth.py:425-429)The GETDEL → re-store pattern when 2. Refresh token TOCTOU window — acceptable tradeoff (auth.py:486-493)
The tradeoff (preventing cross-client DoS) is worth it, but if you wanted belt-and-suspenders, you could use the same GETDEL + re-store-on-mismatch pattern from 3. Revocation TTL boundary condition — cosmetic (auth.py:526-527)remaining = max(0, (token.expires_at or 0) - int(time.time())) + 60
ttl = remaining if remaining > 60 else self._token_verifier._revocation_ttlWhen 4.
|
- Make load_refresh_token atomic via GETDEL, re-store on client mismatch - Rename MAX_CSV_CACHE_BYTES to MAX_CSV_CACHE_CHARS (len() counts chars) - Expose revocation_ttl as property, stop crossing class boundary - Use lazy %s logger formatting in app.py startup error - Add HEALTHCHECK to Dockerfile, remove redundant STOPSIGNAL Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move MAX_INLINE_ROWS, MAX_INLINE_DATA_BYTES, MAX_SCHEMA_PROPERTIES from module-level constants in models.py to Settings fields (max_inline_rows, max_inline_data_bytes, max_schema_properties) so they can be overridden via environment variables. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… tests - _decode_jwt reads alg from signing_key._jwk_data instead of hardcoding RS256, fixing ES256 verification for Supabase projects using ECDSA - Falls back to RS256 when _jwk_data is absent (backwards compatible) - test_http_real.py now uses the shared fake_redis fixture from conftest instead of requiring a manually started Redis on port 6379 - Revert SameSite=strict back to lax on auth cookies (strict breaks the OAuth callback redirect flow from Supabase) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show "Selected all N rows" or "Selection cleared" toast when clicking the select-all button, matching the existing toast pattern used by copy and cell-copy actions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Addresses all 10 findings from the Claude code review on PR #195, plus additional hardening.
Security & auth:
Infrastructure:
"*"), addAllow-Headers@lru_cachesingleton with explicit getter/setter for test safetyesc()XSS helper to session widgetMCP_SERVER_URLto.env.example.dockerignore, Sentinel support in docker-composeTest plan
🤖 Generated with Claude Code