Skip to content

[mcp-2026] CIMD: deny host mismatch and delete duplicate helpers #26

Description

@manish-wekan

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:

  1. nitrostack/auth/cimd.py defines looks_like_cimd_url and resolve_cimd_sync twice (identical copy-paste at end of file).
  2. 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.
  3. 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.
  4. 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.pyapply_cimd_to_registration_body
  • nitrostack/transports/proxy.pycimd_url_matches_request_host (keep inbound trusted-proxy behavior)
  • nitrostack/auth/request.pyverify_bearer_payload
  • tests/test_mcp20_oauth_cimd.py — host match tests ~211–230; they currently allow mismatch

Implementation

  1. Delete the duplicate looks_like_cimd_url / resolve_cimd_sync block. One definition each.
  2. 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.
  3. 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).
  4. 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:

  1. Host mismatch raises (update tests that currently assert ... is False then succeed).
  2. looks_like_cimd_url defined once: e.g. count def looks_like_cimd_url in cimd.py == 1 (simple unit or grep in review).
  3. Existing: blocked RFC 6890 IP, redirects, oversized body, client_id URL mismatch still fail.
  4. 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

  • Duplicate functions gone
  • CIMD host mismatch is CimdValidationError (or equivalent), not a successful registration body
  • Outbound CIMD peer must be the pinned IP
  • tests/test_mcp20_oauth_cimd.py updated and green
  • SSRF tests still fail closed

Related

Independent of sidecar removal. Identity extraction is used by task access (WP3) — do not break “unsigned userId is not identity.”

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions