MCP Server Zero-Day: A 6-Hour Incident Timeline and What We Learned
In early 2026, a widely-installed MCP server for reading Jira tickets was actively exploited through a path traversal vulnerability in its primary tool. The vulnerability had been flagged by static analysis 14 days before exploitation. This is a reconstructed timeline of that incident — from the first anomaly alert to patched release — and the operational lessons that MCP server maintainers should take from it.
Background: the server and the vulnerability
The server: jira-mcp-reader, a Node.js MCP server that allowed AI agents to fetch Jira issues, search projects, and list sprints. 23,000 weekly npm downloads. Used in internal agent toolkits at dozens of companies. The server had a SkillAudit grade of C — a grade that means "elevated risk: conditional use only."
The vulnerability: The fetch_issue tool accepted a Jira issue ID (e.g. PROJ-1234) as its primary argument. Internally, the tool constructed a file path to look up a local cache of recently-fetched issues before hitting the Jira API. The path construction looked like this:
// Simplified — the actual bug
async function fetchIssue(id: string) {
const cachePath = path.join(CACHE_DIR, id + '.json');
if (fs.existsSync(cachePath)) {
return JSON.parse(fs.readFileSync(cachePath, 'utf8'));
}
// ...fetch from Jira API
}
The id parameter was never validated beyond a type check. A caller could pass ../../etc/passwd as the ID, and the server would attempt to read /var/cache/jira-mcp/../../etc/passwd — resolving to /etc/passwd. On Linux, /etc/passwd is world-readable. The response would be parsed as JSON (failing), causing the server to fall through to the Jira API call — but the file read had already occurred and the error handling exposed the file contents in certain configurations.
SkillAudit had detected this as a HIGH: path traversal in tool argument finding 14 days before active exploitation began. The finding was visible in the project's scan results. No one had acted on it.
Tool argument used in fs.join without path.resolve + containment check.
Attack: caller passes ../../etc/passwd, server reads arbitrary files.
Fix: validate id matches /^[A-Z]+-\d+$/ before cache lookup.
The first alert
The server's anomaly detection middleware fired. The fetch_issue tool had been called 247 times in 88 seconds from a single session token — far above the per-session budget of 30 calls per minute. The circuit breaker had tripped and returned 429 Too Many Requests to the caller after the 30th call, but the calls had kept coming, suggesting an automated attacker probing rate-limit enforcement.
The on-call engineer (who was also the primary maintainer) received a PagerDuty alert: "Tool call anomaly: fetch_issue — 247 calls / 88s from session ce4f2a1b."
Triage: not a loop, active exploitation
The engineer pulled the session's tool call log. The id arguments were not Jira ticket IDs — they were a systematic sweep of path traversal payloads:
../etc/passwd
../../etc/passwd
../../../etc/passwd
../etc/shadow
../../etc/shadow
../proc/self/environ
../../proc/self/environ
../proc/self/maps
../proc/version
../var/log/auth.log
...247 more entries
The IP address was a Tor exit node. The user account that had issued the session token had been registered six hours earlier with a disposable email address. This was not a runaway agent. This was an attacker methodically testing the depth and breadth of path traversal exploitation.
The good news: the server's error handling had been partially hardened — JSON parse failures on binary or non-JSON files caused the tool to return a structured error rather than raw file contents. But the /proc/self/environ traversal had succeeded: environment variables are newline-separated ASCII, which the error-path JSON.stringify had partially serialized before hitting a size limit.
/proc/self/environ — environment variables including the JIRA_API_TOKEN the server used for upstream API calls.
Scope assessment
Three questions demanded immediate answers:
1. How many installs are vulnerable? npm download stats showed 23,000 weekly downloads. The vulnerable code had been present since v1.2.0, released six months earlier. Best estimate: 40,000–60,000 active installations across v1.2.0 through v1.4.1 (the current release).
2. What could the attacker do with the stolen JIRA_API_TOKEN? The server's Jira token had full read-write access to all projects in the org's Jira workspace — not the scoped read-only access it should have had. The Jira API allowed issue creation, project configuration changes, and webhook registration. An attacker with the token could create issues, modify project schemes, or register webhooks to exfiltrate future activity.
3. Had the attacker already exfiltrated beyond the single session? Log review showed this was the first exploitation attempt. The attacker's Tor session had ended after the anomaly detection tripped the rate limiter. No Jira API calls had been made using the stolen token in the preceding hour.
The Jira API token was immediately rotated. The JIRA_API_TOKEN was revoked and a new minimal-scope token (read-only, single project) was provisioned and deployed via the secrets manager.
The SkillAudit scan results were re-read. The HIGH path traversal finding from 14 days ago was accompanied by a note: "Credential exposure risk elevated: JIRA_API_TOKEN in environment variables is accessible via path traversal to /proc/self/environ." The scanner had predicted the exact attack path.
Pre-notification of downstream integrators
The responsible disclosure model of a 90-day embargo made no sense for an actively-exploited vulnerability. Three large downstream consumers were identified: a Slack bot that used jira-mcp-reader as its tool backend (serving ~400 users), a VS Code extension bundling the server for internal developer use (3,000 installations), and an internal deployment at a healthcare SaaS company.
Each was contacted directly via their security@ address and maintainer email with:
- Confirmation of active exploitation in the wild
- The specific vulnerability (path traversal in
fetch_issue.id) - Immediate mitigation: rotate any JIRA_API_TOKEN and all other env vars on affected servers
- ETA for patched release: within 5 hours
- A temporary workaround: add a validation shim before the cache lookup
All three responded within 45 minutes. The Slack bot team immediately rotated credentials. The VS Code extension team added the validation shim to their private fork within 90 minutes. The healthcare company isolated their deployment from external traffic pending the patch.
Patch development
The fix was simple — three lines changed, two assertions added. The root cause was that the id parameter had never been validated against the expected Jira issue ID format (PROJ-1234). The cache path was constructed from unsanitized input.
// Before (vulnerable)
const cachePath = path.join(CACHE_DIR, id + '.json');
// After (fixed)
const ISSUE_ID_RE = /^[A-Z][A-Z0-9]+-\d+$/;
if (!ISSUE_ID_RE.test(id)) {
throw new McpError(
ErrorCode.InvalidParams,
`Invalid issue ID format: ${JSON.stringify(id)}`
);
}
const safeName = id.replace(/[^A-Z0-9-]/g, '_');
const cachePath = path.join(CACHE_DIR, safeName + '.json');
const resolved = path.resolve(cachePath);
if (!resolved.startsWith(path.resolve(CACHE_DIR))) {
throw new McpError(ErrorCode.InvalidParams, 'Path traversal detected');
}
The fix also added a secondary containment check: even with the allowlist regex, the resolved path is verified to start within the cache directory. Defense in depth.
A second fix was applied to the credential handling: the JIRA_API_TOKEN was moved out of environment variables and into a secrets manager integration with the token injected only at the function level, not exposed to the process environment. This addressed the secondary vulnerability that made the traversal more dangerous.
See the secrets management post for the full pattern on keeping credentials out of /proc/self/environ.
Testing and staging validation
A regression test suite for path traversal was written before the fix was committed. Eleven test cases:
- Valid IDs:
PROJ-1,MYPROJ-9999,A1-1— all should resolve from cache - Path traversal attempts:
../etc/passwd,../../etc/shadow,PROJ-1/../../etc/passwd— all should throwInvalidParams - Null byte injection:
PROJ-1\x00.json— should throw - Unicode normalization:
PROJ‐1(non-ASCII hyphen) — should throw - Extremely long ID: 10,000-character string — should throw before path construction
All 11 tests passed on the patched code. The entire test suite (147 tests) remained green.
A SkillAudit rescan of the patched code was run in pre-production. The HIGH path traversal finding was cleared. Grade changed from C to A.
Coordinated disclosure and patched release
v1.5.0 was tagged and published to npm. The release notes contained a prominently-placed security notice:
A GitHub Security Advisory (GHSA) was filed using the repository's Security tab. The advisory described the vulnerability class, affected versions, the fix, and the workaround (validation shim). A CVE was requested via GitHub's CNA process — assigned within 3 hours.
The Slack bot team and VS Code extension team were notified that the patched release was live. Both deployed within the next 90 minutes.
The disclosure was also posted to the #mcp-security community Discord and the npm package's GitHub Discussions, ensuring that any users monitoring those channels received immediate notice.
Force-deprecation of vulnerable versions
All vulnerable versions (v1.2.0 through v1.4.1) were deprecated on npm with a message pointing to v1.5.0. Users running npm audit against projects using the deprecated versions would now see a HIGH vulnerability advisory.
npm deprecate jira-mcp-reader@"<1.5.0" \
"SECURITY: path traversal in fetch_issue (CVE-2026-XXXX). Update to v1.5.0."
The three pre-notified integrators were each confirmed as updated. The Slack bot had been running the patched version for 45 minutes. The VS Code extension had published a hotfix release. The healthcare company had completed a staged rollout and lifted their traffic isolation.
One additional consumer was discovered via GitHub code search: a CI/CD pipeline tool that imported the MCP server as a library. The team was contacted and updated within 2 hours.
Post-incident review
Six hours from first alert to all known consumers on patched versions. A short timeline — possible only because anomaly detection fired early and the patch was mechanically straightforward. The post-incident review identified three systemic failures and three process changes:
Failure 1: no alert on scan findings. The HIGH path traversal finding had been in the SkillAudit results for 14 days. No one had set up a notification for HIGH or CRITICAL findings. The scan was running — the results were just not being acted on.
Failure 2: over-scoped credentials. The JIRA_API_TOKEN had full read-write access to all projects. Minimum necessary scope (read-only, single project) would have eliminated the secondary impact of the credential theft.
Failure 3: credentials in process environment. Storing the API token in environment variables made it accessible via /proc/self/environ once path traversal was possible. Secrets manager injection at call time would have limited the exposure window to active call execution, not the full process lifetime.
Process change 1: SkillAudit integrated into CI with a B-grade gate. Any release below B is blocked from publishing. This would have prevented v1.2.0–v1.4.1 from ever reaching npm.
Process change 2: 48-hour SLA for HIGH findings. Any HIGH finding in a scan result triggers a GitHub issue assigned to the maintainer with a 48-hour closure deadline.
Process change 3: Credentials moved to AWS Secrets Manager. No credentials in environment variables; tokens fetched at call time with 15-minute TTL.
Five lessons every MCP server maintainer should take from this
1. Input validation in tool parameters is the highest-leverage control
Tool parameters are direct attacker input. Every tool argument that becomes part of a file path, SQL query, shell command, or API URL must be validated against the strictest allowlist you can define. For ID parameters: enforce the expected format with a regex before the argument touches any system resource. See input validation patterns for a complete treatment of how to build validation into the tool handler lifecycle.
2. Scan findings with no alert are scan findings with no effect
Running a security scanner and not acting on its output is worse than not scanning at all — it gives you false confidence. A HIGH finding that sits for 14 days is a HIGH finding that gets exploited. Configure your scanning workflow so that HIGH and CRITICAL findings create immediate, assigned issues with fixed SLAs. The pre-deployment security review checklist includes a step for this: scan results must be reviewed and triaged before any release proceeds.
3. Anomaly detection saved the incident from being worse
Without the per-session tool call budget and circuit breaker, the attacker's 247-call sweep could have continued until they found a working traversal path that returned parseable data directly. The anomaly detection fired at call 31 and tripped the rate limiter — limiting the exploit window to approximately 88 seconds. Tool call anomaly detection is not optional for production MCP servers.
4. Credential scope directly controls breach impact
The JIRA_API_TOKEN had full read-write access to all projects. If it had been scoped to read-only access on the single project the server needed, the attacker's stolen token would have had dramatically reduced utility. The secrets management post covers minimum-scope credential provisioning — the rule is: request only the permission the tool needs for its specific operation, not the permission that's convenient to share across tools.
5. Responsible disclosure timelines compress to zero for active exploitation
The standard 90-day embargo exists to give maintainers time to patch before public disclosure. Once a vulnerability is being actively exploited, that timeline collapses: affected users need to know immediately so they can rotate credentials and update. A good incident response plan includes pre-identified channels for rapid notification — key downstream integrators, security@, npm deprecation, GitHub advisory — so you can execute the disclosure in parallel with the patch, not after it.
What SkillAudit's scan would have caught before exploitation
The path traversal was not a subtle bug. It was detectable by static analysis: an unsanitized string from a tool argument being used as a filesystem path component. SkillAudit's scanner identified it as a HIGH finding in its first scan of the codebase. The specific findings that mapped to this incident:
- HIGH: path_traversal — tool argument used in fs.join without path.resolve + containment check
- HIGH: credential_in_env — API key stored in process.env, accessible via /proc/self/environ on path traversal
- MEDIUM: credential_scope_excessive — token appears to have write scope based on API endpoints called; read-only alternatives available
Three findings. All three were directly load-bearing in the attack. All three were in the C-grade scan result that had been ignored for two weeks. If those findings had triggered a CI gate that blocked the v1.2.0 release until they were resolved, the incident would not have occurred.
An A-grade MCP server is not a server with no bugs — it is a server where HIGH and CRITICAL findings are resolved before they reach production, where credentials are scoped and protected, and where anomaly monitoring catches exploitation attempts early. The SkillAudit scanner gives you a pre-deployment view of your attack surface. What you do with the results determines whether a scan finding becomes a historical data point or a 6-hour incident.
For a structured way to turn scan findings into remediation work, see the C-to-A remediation plan. For the automated CI integration that enforces grade gates before publish, see the GitHub Action security gate.
Run a pre-deployment scan on your MCP server
Catch path traversal, credential exposure, and 50+ other security findings before they reach production. SkillAudit gives you a letter grade and a prioritized remediation list in under 2 minutes.
Scan your server →