Context
CIMD in #21 is the strongest part of the merge: HTTPS-only (HTTP on loopback), no userinfo/fragment/./.., DNS pre-resolve, RFC 6890 IP blocks, connect to pinned IP, no redirects, 5 KiB cap, document client_id must equal fetch URL, trusted-proxy rules for inbound X-Forwarded-Host.
Gaps:
nitrostack/auth/cimd.py defines looks_like_cimd_url and resolve_cimd_sync twice (identical copy-paste at end of file).
apply_cimd_to_registration_body (nitrostack/auth/oauth_module.py) sets _cimd_host_matches_request and returns the document when False. Host pin is telemetry, not a deny. Tests in tests/test_mcp20_oauth_cimd.py assert the flag is False and continue.
cimd_peer_is_acceptable allows a trusted proxy as TCP peer even when it is not the pinned IP. Outbound CIMD is create_connection((pinned_ip, port)) — that exception must not apply to outbound fetch sockets.
verify_bearer_payload (nitrostack/auth/request.py) except Exception around DI + JWT verify. Missing JWT or programming errors look like “anonymous.”
Keep the SSRF defenses. Do not rewrite OAuth introspection or DCR static client issuance except where CIMD registration must fail closed.
Handoff: #21 (comment)
This issue is largely independent of WP0 (sidecar). Can ship in parallel.
Goal
Host mismatch is an error. One definition of each CIMD helper. Outbound pin is strict. JWT/DI failures are not silently None when they are bugs.
Probe
rg -n "def looks_like_cimd_url|def resolve_cimd_sync|cimd_peer_is_acceptable|_cimd_host_matches_request" nitrostack
Read:
nitrostack/auth/cimd.py — especially the last ~40 lines (duplicates), assert_safe_fetch_target, _PinnedHTTPSConnection, cimd_peer_is_acceptable
nitrostack/auth/oauth_module.py — apply_cimd_to_registration_body
nitrostack/transports/proxy.py — cimd_url_matches_request_host (keep inbound trusted-proxy behavior)
nitrostack/auth/request.py — verify_bearer_payload
tests/test_mcp20_oauth_cimd.py — host match tests ~211–230; they currently allow mismatch
Implementation
- Delete the duplicate
looks_like_cimd_url / resolve_cimd_sync block. One definition each.
- In
apply_cimd_to_registration_body: if request headers/peer are provided and cimd_host_matches_request is False, raise CimdValidationError. Do not return _cimd on mismatch.
- Outbound
cimd_peer_is_acceptable: peer must equal pinned IP (and not be blocked). Do not treat TRUSTED_PROXIES as a substitute destination for CIMD fetch. Inbound Host vs CIMD URL comparison may still use trusted forwarded host (proxy.py).
verify_bearer_payload: catch JWT verification failure only (invalid token → None). Missing JWTService / DI misconfig should not be a bare except Exception. Empty identity on bad token stays correct.
Do not weaken: IP allowlists, redirect block, size cap, client_id equality, HTTPS requirement.
Out of scope: @cache decorator skipping ExecutionContext in cache keys (additional_decorators.py); OAuth OAUTH_REQUIRED fail-open.
Test / verify
pytest tests/test_mcp20_oauth_cimd.py tests/test_oauth.py -q
Change/add:
- Host mismatch raises (update tests that currently
assert ... is False then succeed).
looks_like_cimd_url defined once: e.g. count def looks_like_cimd_url in cimd.py == 1 (simple unit or grep in review).
- Existing: blocked RFC 6890 IP, redirects, oversized body,
client_id URL mismatch still fail.
- Optional: pin connect still uses pinned IP (
test_fetch_connects_to_pinned_ip or equivalent).
Review
- Deny on mismatch is in
apply_cimd_to_registration_body (or a helper it always calls), not only a test flag.
- Trusted proxies still cannot rebind inbound Host via untrusted
X-Forwarded-Host.
- No new HTTP client dependency.
Success
Related
Independent of sidecar removal. Identity extraction is used by task access (WP3) — do not break “unsigned userId is not identity.”
Context
CIMD in #21 is the strongest part of the merge: HTTPS-only (HTTP on loopback), no userinfo/fragment/
./.., DNS pre-resolve, RFC 6890 IP blocks, connect to pinned IP, no redirects, 5 KiB cap, documentclient_idmust equal fetch URL, trusted-proxy rules for inboundX-Forwarded-Host.Gaps:
nitrostack/auth/cimd.pydefineslooks_like_cimd_urlandresolve_cimd_synctwice (identical copy-paste at end of file).apply_cimd_to_registration_body(nitrostack/auth/oauth_module.py) sets_cimd_host_matches_requestand returns the document when False. Host pin is telemetry, not a deny. Tests intests/test_mcp20_oauth_cimd.pyassert the flag is False and continue.cimd_peer_is_acceptableallows a trusted proxy as TCP peer even when it is not the pinned IP. Outbound CIMD iscreate_connection((pinned_ip, port))— that exception must not apply to outbound fetch sockets.verify_bearer_payload(nitrostack/auth/request.py)except Exceptionaround DI + JWT verify. Missing JWT or programming errors look like “anonymous.”Keep the SSRF defenses. Do not rewrite OAuth introspection or DCR static client issuance except where CIMD registration must fail closed.
Handoff: #21 (comment)
This issue is largely independent of WP0 (sidecar). Can ship in parallel.
Goal
Host mismatch is an error. One definition of each CIMD helper. Outbound pin is strict. JWT/DI failures are not silently
Nonewhen they are bugs.Probe
rg -n "def looks_like_cimd_url|def resolve_cimd_sync|cimd_peer_is_acceptable|_cimd_host_matches_request" nitrostackRead:
nitrostack/auth/cimd.py— especially the last ~40 lines (duplicates),assert_safe_fetch_target,_PinnedHTTPSConnection,cimd_peer_is_acceptablenitrostack/auth/oauth_module.py—apply_cimd_to_registration_bodynitrostack/transports/proxy.py—cimd_url_matches_request_host(keep inbound trusted-proxy behavior)nitrostack/auth/request.py—verify_bearer_payloadtests/test_mcp20_oauth_cimd.py— host match tests ~211–230; they currently allow mismatchImplementation
looks_like_cimd_url/resolve_cimd_syncblock. One definition each.apply_cimd_to_registration_body: if request headers/peer are provided andcimd_host_matches_requestis False, raiseCimdValidationError. Do not return_cimdon mismatch.cimd_peer_is_acceptable: peer must equal pinned IP (and not be blocked). Do not treatTRUSTED_PROXIESas a substitute destination for CIMD fetch. Inbound Host vs CIMD URL comparison may still use trusted forwarded host (proxy.py).verify_bearer_payload: catch JWT verification failure only (invalid token →None). MissingJWTService/ DI misconfig should not be a bareexcept Exception. Empty identity on bad token stays correct.Do not weaken: IP allowlists, redirect block, size cap,
client_idequality, HTTPS requirement.Out of scope:
@cachedecorator skippingExecutionContextin cache keys (additional_decorators.py); OAuthOAUTH_REQUIREDfail-open.Test / verify
Change/add:
assert ... is Falsethen succeed).looks_like_cimd_urldefined once: e.g. countdef looks_like_cimd_urlincimd.py== 1 (simple unit or grep in review).client_idURL mismatch still fail.test_fetch_connects_to_pinned_ipor equivalent).Review
apply_cimd_to_registration_body(or a helper it always calls), not only a test flag.X-Forwarded-Host.Success
CimdValidationError(or equivalent), not a successful registration bodytests/test_mcp20_oauth_cimd.pyupdated and greenRelated
Independent of sidecar removal. Identity extraction is used by task access (WP3) — do not break “unsigned userId is not identity.”