DEV Community

Neeraj Kumar Singh Beshane
Neeraj Kumar Singh Beshane

Posted on Originally published at linkedin.com

The Jira Ticket That Read the Server

A Jira ticket is supposed to describe work. It is not supposed to read your server.

On August 12, NVD published CVE-2026-73498, whose advisory confirms an agent doing exactly that. The agent read instructions in a Jira ticket, called an MCP upload tool, opened /proc/self/environ, and attached the result to Confluence. That file can contain live credentials. The initiating interaction was an ordinary ticket review.

No malware, shell, or broken authentication was required. The exploit used a valid tool call and the identity already assigned to the MCP server.

This was a confirmed proof-of-concept, not a reported production breach. That distinction matters, but so does the uncomfortable part: a conventional review could approve every visible piece and still miss the transfer of authority.

The call was valid. The authority was not

MCP Atlassian is a server that connects agents to Confluence and Jira. Before version 0.22.0, its upload tool accepted a client-supplied path and opened it without the safe-path check already used by the download path.

The schema said file_path was a string. It was. Authentication could pass. The tool was allowed. None of those checks answered whether text read from Jira should be able to spend the server's filesystem authority.

The ticket supplied the intent, the runtime supplied the permission, and Confluence made the result durable.

That is more than a path-validation bug. It is an authorization review that stopped at the function signature.

Four questions the schema cannot answer

The turn is simple: stop reviewing the tool declaration as the boundary. For every consequential call, trace four things:

  1. Origin: What caused the call? A user request, Jira content, retrieved text, or another agent?
  2. Authority: Whose identity pays for it? A user token, service account, or delegated credential?
  3. Consequence: What can leave or change? A file, persistent record, external system, or network destination?
  4. Time: Does the same policy run on retries, pollers, and background jobs?

A Brij-style editorial infographic shows the four questions an MCP schema cannot answer, maps three advisories to those questions, and ends with three release probes.

A schema describes the call shape. It does not grant permission.

Three advisories, four missing answers

When I review a large tool plane, I do not ask only whether the tool is authenticated. I trace the origin, authority, consequence, and time path until I can point to the exact denial.

The Atlassian proof-of-concept failed the first three questions. Untrusted Jira content caused the call. The server identity could read the process environment. The consequence was a credential-bearing file copied into a tenant-visible attachment.

Dynatrace MCP is a server that exposes Dynatrace operations as agent tools. Five write tools requested human approval. create_dynatrace_notebook did not. Before 1.8.7, reaching that tool was enough to create a persistent notebook without operator consent. The advisory rates this LOW. The durable lesson is not the score. It is that authentication answered who called, while no control answered who approved the write.

FrontMCP is a framework that turns OpenAPI definitions into MCP tools. Its initial URL load used an SSRF guard. Its optional background poller used raw fetch(). Before 1.5.6, an attacker-influenceable specification URL could make that poller reach loopback, private networks, or cloud metadata endpoints. Polling was off by default. The first request had policy. The later request did not.

The products and severity differ, but the review error is the same: policy covered the declared call, not every place its authority became action.

Put the guard where the consequence happens

NOT ACCEPTABLE

def upload(file_path):
    return open(file_path, "rb")

async def create_notebook(content):
    return await write(content)

async def poll(url):
    return await fetch(url)
Enter fullscreen mode Exit fullscreen mode

ACCEPTABLE

def upload(file_path, allowed_root):
    path = Path(file_path).resolve()
    path.relative_to(allowed_root)
    return path.open("rb")

async def create_notebook(content, approve):
    if not await approve(content):
        raise PermissionError("approval required")
    return await write(content)

async def poll(url, policy):
    return await safe_fetch(url, policy=policy)
Enter fullscreen mode Exit fullscreen mode

Production implementations also need symlink handling, redirect revalidation, resolved-IP controls, scoped credentials, and observable denials. Those details belong in the implementation. The review model stays small enough to use in a pull request.

New here? Securing the Agentic Stack follows the trust boundaries that appear when software can turn what it reads into action.

Run the four-question review

Pick one write-capable MCP tool. Record its origin, runtime identity, concrete consequence, and every later execution path. Then run three controlled probes:

  1. Supply a file path outside the allowed root. Prove denial happens before the file opens.
  2. Invoke a persistent write without approval. Prove no artifact appears.
  3. Point a recurring fetch at a loopback canary. Prove the canary receives zero requests.

That produces a useful release receipt: four answers about authority and three facts from the runtime.

A tool schema tells the model how to call a function. Only a denial at the file, write, or fetch boundary proves the action was allowed.

Next week I am tracing the evidence left after that decision: whether an audit log can prove which policy version allowed it.

Which question is weakest in your MCP review today: origin, authority, consequence, or time?

  • Neeraj

Top comments (0)