Fix CIMD redirect allowlist bypass and cache revalidation#3098
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d776b8d571
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| now = time.time() | ||
| policy = self._parse_cache_policy(response.headers, now) | ||
|
|
||
| if response.status_code == 304: | ||
| if cached is None: | ||
| raise CIMDFetchError( | ||
| "CIMD server returned 304 Not Modified without cached document" | ||
| ) | ||
|
|
||
| if not policy.no_store: | ||
| self._cache[client_id_url] = _CIMDCacheEntry( | ||
| doc=cached.doc, | ||
| etag=policy.etag or cached.etag, | ||
| last_modified=policy.last_modified or cached.last_modified, | ||
| expires_at=policy.expires_at, | ||
| must_revalidate=policy.must_revalidate, |
There was a problem hiding this comment.
Preserve cached cache-control on 304 without headers
When a conditional request gets a 304, you rebuild cache policy solely from the 304 response headers. If the origin omits cache headers on the 304 (common), _parse_cache_policy falls back to the default TTL and clears must_revalidate, overwriting the stricter directives from the cached 200. For example, a cached response with Cache-Control: max-age=0 will be replaced by DEFAULT_CACHE_TTL_SECONDS after a 304 without cache headers, letting clients skip revalidation for ~5 minutes against the origin’s intent. This should reuse the cached policy when the 304 doesn’t provide updated cache headers (RFC allows 304 to only include changes).
Useful? React with 👍 / 👎.
WalkthroughReworks CIMD fetching and OAuth proxy redirect handling. Adds SSRFFetchResponse and ssrf_safe_fetch_response (ssrf_safe_fetch now delegates to it). Replaces simple TTL caching with HTTP-cache semantics via internal _CIMDCacheEntry and _CIMDCachePolicy, parses Cache-Control/Expires, supports ETag/Last-Modified, conditional GETs, 304 handling, and no-store eviction. Updates header parsing and error handling. Adjusts oauth-proxy models to resolve a single CIMD-provided redirect_uri and validate it against configured allowed_redirect_uri_patterns. Updates docs to note CIMD enabled by default. Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/fastmcp/server/auth/cimd.py (2)
210-259: Cache-Control parsing is solid; consider also matching the literalmust-revalidatedirective.Currently only
no-cachesetsmust_revalidate. The HTTPmust-revalidatedirective (a distinct Cache-Control token) has slightly different semantics — it forbids serving stale responses without revalidation — but in this implementation stale entries always trigger a refetch anyway (line 309 falls through), so the omission is safe today. If that staleness-fallback behavior ever changes, this would silently regress.Consider:
no_store = "no-store" in directives - must_revalidate = "no-cache" in directives + must_revalidate = "no-cache" in directives or "must-revalidate" in directives
337-339: Ruff TRY003: inline exception message.Static analysis flags the long string literal in the
CIMDFetchErrorraise. This is a minor style nit — the message is clear and used in a single place. You could suppress with# noqa: TRY003or move the message to the exception class if you prefer to stay lint-clean.
Test Failure AnalysisSummary: Windows tests are timing out during Root Cause: The Suggested Solution: Follow the pattern used in from key_value.aio.stores.memory import MemoryStore
def test_authkit_domain_https_prefix_handling(self):
storage = MemoryStore()
provider1 = WorkOSProvider(
client_id="test_client",
client_secret="test_secret",
authkit_domain="test.authkit.app",
base_url="https://myserver.com",
jwt_signing_key="test-secret",
client_storage=storage, # Add this
)
# ... repeat for provider2 and provider3This eliminates disk I/O entirely during tests and aligns with existing test patterns. Detailed AnalysisStack Trace: Why This Happens:
Existing Patterns:
Related FilesFiles Requiring Changes:
Reference Implementation:
Test Fixtures Available:
|
CIMD follow-up review found two redirect validation inconsistencies in the proxy path: allowlist constraints could be bypassed when a CIMD client omitted
redirect_uriwith a single configured URI, and an explicit empty allowlist did not behave as deny-all for CIMD redirects. The same review also showed CIMD document fetching was still fixed-TTL only, even though the feature now depends on HTTP cache directives to keep metadata fresh without unnecessary refetches.This update makes CIMD redirect enforcement consistent with proxy policy in both omitted and explicit
redirect_uriflows, adds conditional HTTP revalidation support (Cache-Control, ETag, Last-Modified, 304, no-store, no-cache) toCIMDFetchervia SSRF-safe response metadata, and clarifies docs thatenable_cimdis default-on whileFalseis an explicit opt-out.