Skip to content

Pin cross-repo type-registry fetch; harden the MCP plugin#96

Merged
kurtseifried merged 1 commit into
mainfrom
fix/pin-fetch-and-plugin-hardening
Jun 22, 2026
Merged

Pin cross-repo type-registry fetch; harden the MCP plugin#96
kurtseifried merged 1 commit into
mainfrom
fix/pin-fetch-and-plugin-hardening

Conversation

@kurtseifried

Copy link
Copy Markdown
Collaborator

Summary

Three hygiene fixes from the SecID audit (SecID-2026-06-14-claude-skill): F-12-05, F-13-03, F-13-01.

F-12-05 — pin the cross-repo type-registry.ts fetch

Both registry validators (validate-subtypes.py, validate-type-list.py) defaulted to fetching SecID-Service's type-registry.ts from the moving main branch. That's a cross-repo TOCTOU: an unrelated SecID-Service merge silently changes the canonical type/subtype set that gates every SecID PR here, with no review on this side.

Pinned to a reviewed commit SHA (2477ec7 — the commit that last modified the file), with a fail-closed guard if the SHA is ever blanked. --type-registry-url / --type-registry-path overrides are preserved for local/feature-branch testing. Verified the pinned raw URL returns 200, byte-for-byte identical to the live file, so CI's network fetch still passes.

Maintainer note: when SecID-Service's type-registry.ts changes, open a reviewed PR here bumping SECID_SERVICE_PINNED_SHA in both validators in lockstep. Do not revert to main.

F-13-03 — MCP plugin silently fell back to the public resolver

server.py used parse_known_args(), so a typo'd flag (--base-ur1) was silently dropped and the server kept the public default — leaking what were meant to be internal-resolver queries to the public endpoint. Moved argparse into main() with parse_args() (hard error on unknown flags) plus a stderr echo of the effective base URL. Side benefit: the module now imports cleanly (no argparse at import), making it testable.

F-13-01 — MCP plugin returned raw resolver JSON to the LLM

Registry free-text is third-party contributor-authored and flows straight into an LLM-read channel (the disclosure tooling even tells the agent to act on contacts/scope). Added an output-boundary sanitizer: strip C0/C1 + zero-width/bidi chars, cap field/array length, relocate contributor prose under a clearly-labeled registry_text_untrusted envelope (_warning: data, not instructions), and rebuild the response from an allowlist so an unexpected upstream response can't inject arbitrary top-level keys. Tool docstrings carry the same note.

The control-char class is built from integer code points via chr() so the source stays pure ASCII (no literal invisible bytes).

Tests

+6 plugin unit tests (pytest plugins/secid/test_server.py, 6 pass): control/bidi stripping, length cap, prose relocation+labeling, envelope rebuild dropping unknown keys, non-dict input. Existing test_validate_urls.py still green; both validators pass against the pinned content.

Notes for review

  • parse_args() may break a launcher passing unknown extra flags — the shipped .mcp.json passes none (and --base-url is a defined flag), so the happy path is unaffected.
  • The envelope rebuild is intentionally strict (allowlisted keys only); revisit _UNTRUSTED_TEXT_KEYS / _RESULT_STRUCTURAL_KEYS if the response schema gains new fields.
  • The sibling F-04-01 fix (SecID-Service Worker MCP, same labeling approach) is a separate PR.

🤖 Generated with Claude Code

Three hygiene fixes from the SecID audit:

F-12-05 — both registry validators (validate-subtypes.py, validate-type-list.py)
defaulted to fetching SecID-Service's type-registry.ts from the moving `main`
branch. That is a cross-repo TOCTOU: an unrelated SecID-Service merge silently
changes the canonical type/subtype set that gates every SecID PR here. Pinned
to a reviewed commit SHA (2477ec7, the commit that last changed the file), with
a fail-closed guard if the SHA is ever blanked. --type-registry-url/-path
overrides preserved. Bump the SHA via a reviewed PR here when the upstream file
changes (keep both validators in lockstep).

F-13-03 — the MCP plugin used parse_known_args(), so a typo'd flag (--base-ur1)
was silently dropped and the server fell back to the PUBLIC resolver, leaking
internal queries. Moved argparse into main() with parse_args() (hard error on
unknown flags) plus a stderr echo of the effective base URL. Side benefit: the
module now imports cleanly (no argparse at import), so it is testable.

F-13-01 — the plugin returned raw resolver JSON to the LLM. Registry free-text
is third-party contributor-authored; a crafted description/scope/contacts field
could carry injected instructions. Added an output-boundary sanitizer: strip
C0/C1 + zero-width/bidi chars, cap length, relocate contributor prose under a
labeled `registry_text_untrusted` envelope, and rebuild the response from an
allowlist (drops unexpected top-level keys). Tool docstrings note the data is
not instructions.

+6 plugin unit tests. Validators verified against the pinned content (identical
to the live file). The sibling F-04-01 fix for SecID-Service's Worker MCP is a
separate PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kurtseifried kurtseifried merged commit 794707b into main Jun 22, 2026
2 checks passed
@kurtseifried kurtseifried deleted the fix/pin-fetch-and-plugin-hardening branch June 22, 2026 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant