modelcontextprotocol / modelcontextprotocol/inspector

Form-encoded secret masking can be defeated by an unescaped & inside the secret value itself

Open
#2,422 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug v2
Dominant language
TypeScript
Stars
10.9k
Forks
1.5k
Avg merge
6h 17m
Merged PRs (30d)
151

Description

Which version line?

v2 — current (@modelcontextprotocol/inspector@latest)

Which client?

Web

Inspector version

2.7.0 (git tag) — static code-review finding, not run locally

Node version

N/A — static code review, no live run performed

Operating system (and browser, for the web client)

N/A — static code review

Transport

Streamable HTTP

MCP server under inspection

N/A — this is a static code-review finding against the 2.7.0 tag source (clients/web/src/utils/maskSecrets.ts), not a live reproduction against a running MCP server.

Steps to reproduce

Found via static review of the 2.7.0 tag source, not a live run.

  1. maskFormBody() (clients/web/src/utils/maskSecrets.ts, lines 119-142) masks a form-urlencoded body by first calling body.split("&") (line 122), then for each resulting piece splitting on the first "=" to get a key and value, and masking the value when the key is one of the sensitive FORM_SENSITIVE_KEYS.
  2. A conforming form-urlencoded body always percent-encodes a literal "&" inside a value (as %26), so split("&") is normally safe. But the function trusts the wire's own encoding; nothing here validates that a value contains no raw "&".
  3. If a non-conforming OAuth/token endpoint returns a body where a secret value (e.g. access_token) contains a raw, un-percent-encoded "&", split("&") breaks that single value into two or more pieces mid-value.
  4. The prefix piece still looks like "access_token=" and gets masked. But the remaining piece(s) — the tail of the secret — have no "=" sign, so eq === -1 (line 125) and the piece is returned completely unmasked (line 125: if (eq === -1) return pair;), landing verbatim in the displayed/masked body.

I have not exercised this against a live non-conforming server; flagging what tracing the function's logic against the 2.7.0 tag source shows.

Expected behavior

Every character of a masked secret value stays out of the displayed Network-tab body, even when the value itself contains a raw, non-percent-encoded "&" from a non-conforming server.

Actual behavior

A raw "&" inside a masked key's value defeats the masking for everything after that character: the tail of the secret is displayed as its own unmasked, no-"=" segment in the body shown in the Network tab, right next to the properly-masked prefix.

Suggested fix: a non-conforming value containing a raw "&" is already a malformed body, so no split on "&" can fully recover intent — but the current behavior (silently pass through a no-"=" segment unchanged) is the worst case, since it looks like an ordinary flag-style parameter rather than a leak. Treat a segment with no "=" sign as suspicious when it immediately follows a masked pair (mask it too, or flag the body as only partially maskable) rather than displaying it untouched.

Logs, errors, or screenshots

No response

Already prototyped a fix?

No response

Before you submit
  • I searched existing issues and this is not a duplicate.
  • This is not a security vulnerability report (those go through the private advisory process).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in clients/web/src/utils/maskSecrets.ts at maskFormBody(), especially the body.split("&") and no-"=" handling described in lines 119-142. Trace how a raw ampersand in a sensitive value reaches the Network-tab display. Done means the complete sensitive value is excluded from the displayed body, including this malformed-body case, with coverage for the behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend, security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.