Skip to content

Low: request-state and JWT signing key material lacks minimum-length enforcement and zeroization #4

Description

@bug-ops

Vulnerability

Two related hardening gaps around signing-key handling for security-sensitive paths:

  1. RequestStateCodec::new (crates/rmcp/src/model/request_state.rs:177) accepts impl Into<Vec<u8>> with no length check. The doc comment recommends "a high-entropy key of at least 32 bytes" but nothing enforces it — a caller can construct a codec with a key as short as b"key" (see the crate's own test at request_state.rs:395, :438, :449, :466, which use 3-byte keys) and it compiles and works normally in production code, not just tests.
  2. Key material is never zeroized on drop. RequestStateCodec.key: Box<[u8]> (model/request_state.rs:164) and the signing_key: Vec<u8> used for private_key_jwt client assertions (transport/auth.rs:931, passed into EncodingKey::from_rsa_pem/from_ec_pem at auth.rs:3205-3206) are plain heap buffers with no Drop impl clearing them, and the crate does not depend on zeroize.

Severity

Low (P3) — defense-in-depth. Neither issue is directly triggerable by a network peer; both require a downstream integrator to misuse the API (pick a weak key, or rely on the process not being memory-inspected/core-dumped) rather than an attacker supplying malicious input.

Location

  • crates/rmcp/src/model/request_state.rs:177 (RequestStateCodec::new, no key-length validation)
  • crates/rmcp/src/model/request_state.rs:164 (key: Box<[u8]> field, no zeroize)
  • crates/rmcp/src/transport/auth.rs:931,3205-3206 (signing_key: Vec<u8> for private_key_jwt, no zeroize)

Attack Scenario

A downstream server operator constructs RequestStateCodec::new(b"short") (nothing in the API prevents it). The resulting HMAC-SHA256 key has far less entropy than the documented 32-byte minimum, making offline brute-force/dictionary recovery of the key from observed seal() outputs meaningfully more tractable than intended, which would let an attacker forge requestState blobs (SEP-2322) and bypass the integrity guarantee the codec exists to provide. Separately, because neither the HMAC key nor the JWT signing key is zeroized on drop, either can linger in freed heap memory and surface in a core dump, swap file, or memory-disclosure bug in a way a zeroize-backed type would prevent.

Remediation

  • Have RequestStateCodec::new return a Result (or otherwise reject) keys shorter than a defined minimum (e.g. 16 or 32 bytes), instead of only documenting the requirement.
  • Wrap RequestStateCodec.key and the private_key_jwt signing_key in a zeroize::Zeroizing<Vec<u8>> (or implement ZeroizeOnDrop) so key material is cleared when dropped.

Upstream

None found — searched modelcontextprotocol/rust-sdk issues/PRs for request_state, RequestStateCodec, zeroize, HMAC key. Related but distinct: modelcontextprotocol/rust-sdk#1096 (open — request-state format lacks a kid for key rotation) and modelcontextprotocol/rust-sdk#1128 (open PR — adds request-state key rotation). Neither addresses minimum-length enforcement or zeroization.

References

None (internal hardening gap, no CVE/RUSTSEC).

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Medium: suboptimal behavior, minor inconsistencybugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions