Skip to content

fix: harden mcp baseline scanner against false negatives and crashes#155

Merged
jithin23-kv merged 3 commits into
masterfrom
fix/baseline-scanner-hardening
Jul 2, 2026
Merged

fix: harden mcp baseline scanner against false negatives and crashes#155
jithin23-kv merged 3 commits into
masterfrom
fix/baseline-scanner-hardening

Conversation

@prasanth-nair-kv

@prasanth-nair-kv prasanth-nair-kv commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

What & why

The MCP pre-flight scanners had four latent security/robustness bugs — pre-existing,
surfaced while reviewing the phase-pipeline extraction (#154), plus one reported by
CodeRabbit. Fixing them all together with tests.

# Bug Impact Fix
1 scanResources used content.startsWith("ERROR: ") to detect a read failure A resource whose legitimate content starts with ERROR: was recorded as an error and skipped from the secret/PII judge — a silent false negative McpTarget.readResource now throws on failure (out-of-band); scanResources try/catches. No in-band sentinel, no collision.
2 Zero-byte baseline hit !baselineJson → treated as "first run" Silently re-recorded, missing a real rug-pull drift Distinguish "no file" from empty/corrupt; empty → ERROR
3 Unguarded JSON.parse(...) as Array<…> of the baseline A corrupt file threw uncaught and aborted the whole run with no report parseBaselineSnapshot() guards it → ERROR, no crash
4 On drift, the trusted baseline was overwritten with the changed snapshot A second run PASSed silently — the rug pull launders itself into the baseline (CodeRabbit) Drift no longer overwrites; the baseline is sticky and keeps flagging FAIL until an operator re-baselines

Design: fail closed

Fixes 2–4 fail closed — an untrusted state (corrupt baseline, or a detected drift)
is never silently promoted to the trusted baseline, which would let a
corrupt-then-drift sequence launder itself (the same class as #4). A genuine first run
(no baseline file) still records and passes.

Backward compatibility

McpTarget.readResource changes contract from "return ERROR: … on failure" to
"reject (throw) on failure". Blast radius is contained: the only caller of the
wrapper is baselineScanner (+ test fakes); run/scanResources.ts uses the raw MCP
client, not the wrapper. The interface JSDoc documents the new contract.

Tests (+6)

  • Drift → FAIL, with the baseline file byte-for-byte unchanged and the second run
    still FAIL
    (the CodeRabbit fix).
  • Empty and corrupt baselines → ERROR (no crash); no-drift → PASS.
  • Both resource paths: legit ERROR: content is judged (not misclassified); a thrown
    read failure surfaces as an ERROR result.

Verification

full suite 127 tests, 0 fail (3 skips) · typecheck 0 · lint 0 · prettier clean · build OK.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved baseline drift handling so existing baselines are no longer overwritten during reruns.
    • Corrupt or empty baseline files now fail safely instead of crashing or being treated as a clean start.
    • Resource read failures are now reported more reliably, with clearer error handling and verdicts.

The MCP pre-flight scanners had four latent security/robustness bugs (pre-existing;
surfaced by review of the phase-pipeline extraction):

1. scanResources used the in-band sentinel `content.startsWith("ERROR: ")` to detect
   a read failure, which collides with a resource whose legitimate content starts
   with "ERROR: " — that resource was recorded as a read error and skipped from the
   secret/PII judge (a silent false negative). McpTarget.readResource now throws on
   failure instead of returning an "ERROR: …" string, and scanResources try/catches.
   Blast radius is contained: the only caller of the wrapper is baselineScanner
   (run/scanResources.ts uses the raw mcp client).
2. scanRugPull treated a zero-byte baseline as a clean "first run" (`!baselineJson`),
   silently re-recording and missing a real rug-pull drift.
3. The baseline was parsed with an unguarded `JSON.parse(...) as Array<…>` — a corrupt
   file threw uncaught and aborted the whole run with no report.
4. On detecting drift, the code overwrote the trusted baseline with the changed
   snapshot, so a second run PASSed silently — the rug pull laundered itself into the
   baseline (reported by CodeRabbit).

Fixes 2–4 fail closed: a new parseBaselineSnapshot() returns null for empty/corrupt/
non-array baselines and the caller reports ERROR without touching the file; detected
drift no longer overwrites the baseline, so every subsequent run keeps flagging FAIL
until an operator explicitly re-baselines (deletes the file). A genuine first run
(no file) still records and passes.

Adds 6 tests: drift stays FAIL with the baseline byte-for-byte unchanged across runs,
empty/corrupt baselines yield ERROR (no crash), no-drift passes, and both resource
read paths (legit "ERROR: " content vs a thrown read failure).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 96cb6982-f1f7-434e-9446-9261767751db

📥 Commits

Reviewing files that changed from the base of the PR and between eaa6c37 and f20d10f.

📒 Files selected for processing (3)
  • core/src/execute/baselineScanner.ts
  • core/src/targets/mcpTarget.ts
  • core/tests/baselineScanner.test.ts
👮 Files not reviewed due to content moderation or server errors (3)
  • core/src/targets/mcpTarget.ts
  • core/tests/baselineScanner.test.ts
  • core/src/execute/baselineScanner.ts

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title succinctly summarizes the main fix to the MCP baseline scanner.
Description check ✅ Passed The description covers the problem, solution, changes, tests, and verification, with only minor template section mismatches.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/baseline-scanner-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/src/execute/baselineScanner.ts`:
- Around line 252-257: The first-run path in baselineScanner’s baseline handling
is too broad because baselineJson === null also includes non-ENOENT read
failures from the earlier baseline read. Update the control flow in
baselineScanner so baselineReadError is checked before the “no baseline” branch,
and only treat ENOENT as a genuine missing baseline that calls recordBaseline()
and passes; for any other read error, return ERROR and do not modify state.
- Around line 358-376: The baseline parser currently only checks that the
snapshot is an array, so invalid items like null can still pass through and
later break computeToolDiffs when it reads t.name. Update parseBaselineSnapshot
to validate every array element against the expected tool shape using Zod, and
return null unless the entire parsed snapshot is an array of valid entries. Keep
the existing fail-closed behavior for empty, malformed, or partially corrupt
snapshots.

In `@core/src/targets/mcpTarget.ts`:
- Around line 114-117: The thrown read failure in mcpTarget’s read path is too
generic because `msg.slice(0, 300)` can omit the resource context and
remediation. Update the error construction near the `throw new Error(...)` in
`mcpTarget` to include the resource URI and a clear next action for the operator
before it reaches `toolError`, while still preserving the original cause via the
`cause` option. Keep the change localized to the read-failure handling logic so
callers of the resource-read flow get an actionable message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 93e7c0ac-2749-43ba-994a-4ae1037d1f8c

📥 Commits

Reviewing files that changed from the base of the PR and between 36fa8af and eaa6c37.

📒 Files selected for processing (3)
  • core/src/execute/baselineScanner.ts
  • core/src/targets/mcpTarget.ts
  • core/tests/baselineScanner.test.ts

Comment thread core/src/execute/baselineScanner.ts Outdated
Comment thread core/src/execute/baselineScanner.ts
Comment thread core/src/targets/mcpTarget.ts Outdated
The rug-pull scan's readFile catch swallowed every error into baselineJson ===
null, so a permission/I-O failure on an existing baseline was treated as a first
run — it re-recorded a fresh baseline and passed, masking drift and clobbering the
unreadable baseline. Capture the error code: only ENOENT records-and-passes; any
other read failure fails closed with an ERROR and leaves the file untouched.

Adds a test using a directory at the baseline path (readFile → EISDIR) to prove
the non-ENOENT path yields ERROR without recording.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- validate every baseline entry with a Zod schema (reject [null] etc.)
- treat a formatting-only baseline diff as no-drift, not a sticky FAIL
- fail closed with ERROR on baseline write failure instead of crashing
- bound the untrusted resource URI in read-error messages
- make the unreadable-baseline reasoning actionable

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jithin23-kv

Copy link
Copy Markdown
Collaborator

Follow-up changes on top of the hardening fixes:

  • Baseline entries now validated with Zod — malformed files (e.g. [null]) fail closed to ERROR instead of crashing.
  • Formatting-only baseline diffs are treated as no-drift, not a sticky FAIL.
  • Baseline write failures report ERROR instead of crashing the run.
  • Resource read errors now include the URI + next step (URI length-bounded).
  • Unreadable-baseline message now tells the operator how to recover.
  • Added tests for the malformed-array and formatting-only cases. Suite green; typecheck/lint/prettier clean.

@jithin23-kv
jithin23-kv merged commit cd36f85 into master Jul 2, 2026
8 checks passed
@jithin23-kv
jithin23-kv deleted the fix/baseline-scanner-hardening branch July 2, 2026 11:47
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.

2 participants