Vulnerability
mcp-atlassian Attachment Upload Could Read Outside the Workspace
GHSA-g5r6-gv6m-f5jv affects mcp-atlassian releases before 0.22.0: the Confluence attachment tool carried a caller-controlled file_path to local open() without first proving that the resolved file still belonged to the workspace, so untrusted content could turn a readable host object into a persistent remote attachment.

In this article
1 The attachment succeeded before anyone asked whether its bytes belonged there
The easiest record to overlook in this incident is a successful business event. A Confluence page acquires an attachment. Its creator is a familiar integration identity, the HTTP exchange finishes normally, and the filename resembles an ordinary build artifact. The page administrator sees a permitted upload. At the same instant, the host may record that the mcp-atlassian process read a file outside the intended project. Each system completed the job assigned to it; the security failure lives in the unasked question between them.
mcp-atlassian exists to make Jira and Confluence useful through the Model Context Protocol. A compatible client can search issues, update content, and publish local outputs as attachments. That last feature necessarily combines two powers: reading bytes from the machine that runs the MCP server and writing those bytes with a preconfigured Atlassian identity. Before version 0.22.0, a file_path could join those powers without a third decision proving that the file was part of the workspace authorized for this tool invocation.
This is not a story about Confluence accepting a dangerous extension. The remote endpoint received a conventional multipart request, applied normal authentication, and created an attachment according to page permissions. The missing constraint was local selection. Absolute paths, parent traversal, or a symbolic link could reach any object readable by the service account. Once uploaded, an ephemeral host read became a named, searchable, downloadable object governed by remote versioning, exports, notifications, and backups.
The advisory demonstrates why that quiet composition matters in an agent workflow. Untrusted Jira content influenced planning, the model selected the Confluence attachment tool, and the supplied path referred to Linux /proc/self/environ. Nothing in the file carries a label saying that it is outside the task. The tool call used valid credentials on both sides. Only a timeline that joins content provenance, the structured argument, the local file handle, and the resulting attachment reveals the full transfer: Jira-originated content caused the MCP server to read a host object and publish those bytes to Confluence.
The code and response decision can be stated in one place. The affected range is mcp-atlassian < 0.22.0. In Confluence, the critical path enters AttachmentsMixin.upload_attachment() in src/mcp_atlassian/confluence/attachments.py and reaches _upload_attachment_direct(), where open(file_path, "rb") creates the local handle. Commit b041733473f95119dd539542a43c280737a8e460, released in 0.22.0, connects validate_safe_path() to the Confluence and Jira upload entries. Permanent repair requires upgrading, restarting every old worker, and narrowing the root represented by Path.cwd() to a dedicated artifact directory. Until then, remove the local attachment tools, revoke remote write, or expose only an empty dedicated staging area to the process. Prompt changes, model replacement, and confirmation dialogs do not substitute for server-side path confinement. If a suspicious attachment already exists, preserve its metadata and call records, restrict remote copies, and rotate credentials according to the content.
The investigation therefore begins at the remote object and walks backward. It identifies the attachment, returns to the MCP request that created it, and then follows the argument through the FastMCP entry point, fetcher, attachment mixin, and final open(). This direction prevents “the model was tricked” from becoming the whole explanation. Model behavior describes activation. Server code establishes the maximum reach of every activation, including calls from other clients, direct library users, or automation.
1.1 A legitimate remote write borrowed an oversized local read
The attachment interface appears compact: a destination content ID, a local path, an optional comment, and a minor-edit flag. The content ID answers where the bytes will be published. The path answers where they will come from. Those inputs carry different authorization semantics. Confluence can decide whether the integration may modify a page, yet it cannot determine whether a host pathname belongs to the current project. The operating system can permit the service account to read a file, yet it cannot determine whether this particular remote publication was approved by the data owner.
When the two local decisions are connected directly, “readable by the process” silently becomes “publishable by the tool.” A desktop child process may inherit access to a home directory, repositories, and credential stores. A container may see a working tree, environment injection, service-account mounts, and diagnostic volumes. A shared MCP server may reuse one process and one Atlassian session for several users. In every shape, the visible attachment button can represent a much larger host view than its business wording suggests.
The remote filename does not preserve that view. A source such as /home/service/.config/example/token.json can arrive as token.json; /proc/self/environ can arrive as environ. If an attachment already has that name, the upload may create a new version. A reviewer who searches only for suspicious extensions or full local paths will miss objects whose most important context disappeared when basename was chosen as the title.
Those three intersecting permissions explain the advisory's 7.7 score more precisely than a generic claim of arbitrary file read. The flaw does not bypass the host ACL, and it does not manufacture Atlassian administrative rights. It connects two powers already granted to the integration. The ceiling is the intersection of three sets: host objects readable by the process, tool parameters controllable by the caller or influenced by content, and remote locations writable and visible through the configured identity.
The repair can therefore preserve the feature. It inserts a stable, testable local root before the file handle is created. Artifacts inside that root continue through the existing upload path. A resolved candidate outside the root stops before local I/O. The guarantee no longer depends on a model choosing a sensible filename: every caller receives the same filesystem limit, while the ordinary artifact workflow remains available.
1.2 The advisory fixes the version and commit; source code proves the middle
GitHub published GHSA-g5r6-gv6m-f5jv on July 10, 2026. It rates the issue High, assigns CVSS 7.7, identifies every release below 0.22.0 as affected, and names 0.22.0 as the first fixed release. No CVE is assigned, so inventories and tickets should retain the GHSA identifier. Waiting for a CVE field would create an artificial delay for a vulnerability that already has a stable advisory and a verifiable upstream fix.
The relevant upstream commit is b041733473f95119dd539542a43c280737a8e460, whose parent is 4067d1db4097755cc6add87f95fe3f0746c98c0b. It is present in tag v0.22.0 and subsequent releases. That commit is a broader security-hardening bundle. This report follows only the attachment-path confinement described by the advisory; combining neighboring fixes into the same causal narrative would enlarge the published claim and make downstream backport verification less precise.
The advisory establishes the affected scope. Source review fills the causal middle. We compare the affected FastMCP declaration, Confluence fetcher, AttachmentsMixin, and direct upload helper with the fixed validator, its Confluence and Jira call sites, and the unit tests. At each layer we ask one question: how does the argument enter, when does it change form, where is the local handle created, and which product entry point now enforces the workspace decision?
Reading the repair at commit level prevents a common mistake: treating the new base64 upload option as the entire repair. Client-supplied bytes can avoid asking the server to select a host file, which is useful. The compatible file_path path still exists, however. Its security property changes because canonical resolution and containment now run before open(). A new argument form alone would leave the older local-file workflow exposed.
We also keep local findings separate from upstream facts. The advisory cannot tell an organization which directory its service manager selected, which volumes its container mounted, or which spaces its token could write at a past moment. Those are deployment measurements. A review table with “confirmed upstream” and “verified locally” columns shows exactly where public evidence ends and where the incident owner must supply configuration or telemetry.
1.3 Working backward from the remote attachment is where the case begins
The attachment is the most visible result, but it is incomplete evidence. It can provide attachment ID, content ID, creator, filename, size, timestamps, version, and page permissions. Some environments can also supply access or download events. Those fields prove that bytes entered a collaboration scope. Because the title usually contains only a basename, they do not reconstruct the local absolute path. Opening the file casually to satisfy curiosity can also increase the number of people exposed to sensitive content.
MCP client or server records should contribute the tool name, session, initiating user, source object, arguments, target content, response, and time. Older logs may include a raw pathname, which is valuable for scoping and sensitive in its own right. Newer telemetry can record a path-policy result, a workspace identifier, and a root-relative object without copying secrets into general logs. If structured records are absent, conversation history, process output, reverse-proxy events, and Atlassian timestamps can still narrow the search.
The host side needs evidence of a read, not a modification. Linux audit data, eBPF, container telemetry, or endpoint file events may show the mcp-atlassian process opening an object near the tool call. Network telemetry then links the same process or container to an outbound Atlassian request. These systems may not share a request ID, so investigators preserve process lifetimes, container restarts, source and destination identities, byte counts, and measured clock offsets instead of joining everything that occurred in the same minute.
If the file may contain credentials, the case extends beyond the attachment. The remote object is a disclosure state; later impact depends on viewers, downloads, notifications, exports, indexing, backups, and subsequent use of the disclosed material. High-value credentials can be revoked and replaced from a trusted environment before the viewer list is complete. That action reduces the live window while the investigation continues, provided the response change is timestamped and distinguished from attacker activity.
The same discipline constrains the public story. We can establish that affected code allowed a readable external path to reach remote upload and that the advisory demonstrated a prompt-injection activation. Whether a particular organization lost data, which objects left, and who viewed them require that organization's telemetry. Separating product capability from case-specific occurrence gives responders decisive engineering guidance without filling evidentiary gaps with invented incident details.
The ordinary attachment at the opening now becomes a state transition. Before the call, its bytes exist only in a host object. During the call, an untrusted pathname becomes a file handle. After the call, the same bytes have a remote identifier, permissions, versions, and retention. Every function, path form, control, and response step that follows explains which authorization decision was missing between those states.
2 Follow file_path through five layers until it becomes a sendable handle
The vulnerable behavior is distributed across several reasonable delegations, not hidden in an obscure algorithm. The tool layer converts arguments into a business call. A fetcher selects the product implementation. A mixin handles names and reuse. A direct helper creates the multipart body. An authenticated HTTP client sends it. Every layer preserves the meaning assigned to file_path at the beginning, so the untrusted string reaches local I/O without being reclassified as a resource choice that needs authorization.
2.1 FastMCP exposes a business action, not a raw filesystem primitive
src/mcp_atlassian/servers/confluence.py registers the asynchronous upload_attachment() tool. The affected interface accepts a Confluence content ID, file_path, an optional comment, and minor_edit. It reads like a familiar upload form. That business vocabulary hides the fact that the server will interpret one field inside its own host namespace with the full read authority of its operating-system account.
The tool obtains a configured Confluence fetcher. That object already contains the base URL, authenticated session, TLS and proxy choices, and other remote state. The call then forwards content ID, path, comment, and edit flag to confluence_fetcher.upload_attachment(). The first identity split has already happened: an MCP session selects the file, while a long-lived integration identity will perform the remote write without asking the caller to provide credentials again.
The tool description accepted absolute paths and explained relative paths through the current working directory. That is convenient for automation because a model can reference a precise artifact. Without an allowed root, the same convenience exposes the service account's host namespace. A value remains typed as a string even though its authority silently expands from a project artifact reference into a selector for any readable host object.
The FastMCP wrapper is not the only route to the underlying method. A library consumer can use the fetcher directly. Batch behavior can split several path values and reuse the attachment layer. A guard placed only in the visible tool would protect one entry while leaving shared implementation callable through another. The fixed design chooses the attachment mixin because every local-path upload must pass that point before the direct helper.
Version 0.22.0 also introduces content_base64, allowing a client to submit bytes it already holds. That option changes who selects the source data: the server no longer opens a host file for that branch. The two modes need mutual exclusion, separate size limits, and careful logging. Base64 reduces the need for server-side path selection, but it does not remove the obligation to confine the compatible file_path mode.
2.2 AttachmentsMixin changed the path's form without changing its authority
The fetcher reaches AttachmentsMixin.upload_attachment() in src/mcp_atlassian/confluence/attachments.py. Affected code turned a relative input into an absolute path, checked existence, and used os.path.basename() for the remote title. Those steps improve functional reliability. They let callers ignore the launch directory and keep a local directory tree out of the visible attachment name.
abspath answers where a string points from the current directory. It does not answer whether that location is authorized. A normal child path becomes a stable address inside the workspace. A traversal value becomes a stable address outside it. An absolute input remains absolute. Existence proves only that an object can be found. The host ACL proves only that the service account may read it. Neither expresses the scope of the current tool request.
The mixin then calls _upload_attachment_direct(). The helper prepares /rest/api/content/{content_id}/child/attachment, applies the fields required by the Atlassian endpoint, and invokes open(file_path, "rb"). That file object becomes the multipart file part. Comment and minor-edit state travel as separate data. This is the first point at which a string causes a real local read.
After a successful request, the remote service returns information such as attachment identity, title, size, and links; an existing name can acquire a new version. The helper closes the file in its cleanup path. From a reliability perspective, the transaction is neat: open, transmit, close, and return. There is no dangling handle or conspicuous application error. The anomalous fact is the identity of the opened object, which ordinary success monitoring does not evaluate.
Batch upload does not create a second root cause. It converts a list into several candidates and sends each through the same mixin. A single external entry simply turns one disclosure into one item in a larger operation. Protecting the mixin means single-file tools, batch conveniences, fetchers, and direct library callers inherit the same rule. A rejected path never reaches the helper and cannot create a partial remote attachment.
File extension, MIME detection, malware scanning, and remote content rules address different risks. Sensitive configuration can use an ordinary text suffix or no suffix at all. A remote scanner evaluates whether bytes are dangerous to consume; it cannot determine whether their local owner permitted publication. Those controls remain useful, but none limits selection to the host workspace.
2.3 Caller, process, and remote identity became misaligned on one argument
At least three principals participate. A user, model session, or automation influences the tool request. The operating-system account determines which local files can be read. An OAuth or API-token identity determines where attachments can be created and who can see them. The affected path never represented the authorized intersection of those principals. If each could complete its local action, the full cross-domain action succeeded.
A shared service amplifies that mismatch. Several callers may use different clients while one process and one remote session are reused for efficiency. A task for project A can borrow a host view that also contains project B, operational configuration, or service secrets, then publish through a destination identity with a separate permission graph. Even without a formal multi-tenant promise, such reuse calls for process, workspace, and credential separation by purpose.
Desktop use is not automatically low risk. A launcher may inherit the user's home, a repository root, or an application directory as the child process cwd. The interface still shows one attachment tool. Once version 0.22.0 makes Path.cwd() the default base, launch location becomes security configuration. Installers and desktop profiles need to create an explicit staging directory and start the child there.
Remote permissions determine the audience. An attachment on a private test page, an internal knowledge base, and a guest-accessible space have different consequences. Permission inheritance, search, notifications, exports, and backup retention can expand the original audience. Incident scope should record space, page, groups, guests, versions, downloads, and retention instead of stopping at the sentence “it was uploaded to Confluence.”
Historical privilege changes belong in the same ledger. Tokens may be rotated or granted new scopes. Pages can move between spaces. Service-account group membership and mounted volumes change with deployments. Current state cannot safely be projected backward. Configuration audit, infrastructure versions, and permission history reconstruct the intersection that existed for each part of the incident window.
For shared deployments, pairing each remote identity with its own process and staging root creates strong operational separation. One instance cannot borrow another team's directory or destination scope. The additional resource cost buys smaller incident radius, unambiguous log ownership, and credentials that can be revoked without suspending every user of the integration.
3 A normalized path can still belong outside the workspace
Path reviews often end too early with the statement that input is normalized. Normalization and confinement are different properties. The first removes ambiguity from how a location is expressed. The second proves that the resolved object lies inside an authorized root. Affected mcp-atlassian code had the first property and lacked the second. It understood the address more clearly and then opened it without an authorization decision about directory membership.
A useful path matrix covers absolute paths, parent traversal, sibling names with a shared prefix, symbolic links, different drives or shares, and launch-directory changes. None is a separate vulnerability class here. They are alternate ways to ask the resolver for a final object outside the allowed tree. The fixed algorithm brings their syntax into one coordinate system and makes the decision on the canonical result.
3.1 abspath locates an object; is_relative_to establishes membership
Assume the process starts in /srv/mcp/workspace. The input reports/build.txt becomes a stable address under that directory. The input ../../etc/hosts also becomes stable, but outside it. Normalization treats both candidates consistently because it has not been given a policy root. In fact, it can make the second access more reliable by removing the very syntax that made the traversal visible to a human reviewer.
String prefixes cannot supply the missing relationship. The allowed directory /srv/work and sibling /srv/work-backup share characters. A text startsWith check can accept the sibling. Windows introduces drive letters, case behavior, UNC paths, junctions, and reparse points. A correct decision uses the platform's path semantics and compares components after resolution, not similar bytes in the original input.
The fix resolves both candidate and base, then calls resolved_path.is_relative_to(resolved_base). That order is part of the guarantee. Comparing before resolution allows parent components or links to change the target afterward. Resolving without comparison merely produces an accurate external address. Canonicalization followed by component-aware containment is what turns the working directory into a capability root.
User expansion and other conveniences deserve the same treatment. If ~ is supported, it can expand to the service account's home, a common location for credentials and configuration. An interface may reject that notation or expand it and still apply containment. Character blacklists are fragile because they target one spelling. A final-object rule makes every accepted spelling answer to the same root.
Names and objects also differ in ways canonical path comparison does not fully govern. Hard links can give one underlying file several directory entries. A mount can attach another filesystem inside the allowed tree. The resolver restricts the namespace; it cannot decide whether every object mounted there is suitable for publication. A service-owned staging area containing copied artifacts is easier to audit than a broad repository decorated with more filename rules.
With strict=False, a missing candidate can still be resolved and checked for membership. The later upload should fail because no readable file exists. Tests need to keep that functional error separate from a policy rejection and confirm that the server does not create an empty file as a side effect. Clear layer ownership protects the “upload reads an existing artifact” expectation if I/O code changes later.
3.2 Absolute paths, parent components, and links are three faces of one escape
An absolute path is the clearest external selection. On Linux it may name a process pseudo-file, a home configuration, a container mount, or a system file. A Windows service may see another drive, a share, or a profile directory. The operating system still applies ACLs. Exploitation does not require defeating them; it requires one object that the service account may read but the current collaboration task was never authorized to publish.
Parent traversal can hide inside a path that looks project-relative. A deeply nested output directory may receive ../../../secrets/example. Removing one textual ../ is incomplete and can damage legitimate names while missing repeated or platform-specific representations. Resolve-and-compare makes the number and spelling of parent components irrelevant to the final decision.
A symbolic link shows why the candidate string may appear fully inside the root and still select an external target. Every component of workspace/export/latest is written under the workspace, while latest can point to a credential file elsewhere. Canonical resolution follows an existing link and exposes the external target to the containment check. A link whose target remains inside the root can continue to support legitimate artifact organization.
Directory links and mounts introduce an operational choice. If an administrator intentionally mounts a large shared volume beneath the working directory, the target may be correctly treated as inside the configured tree. Code cannot infer which portion of that volume is publishable. The deployment must make the base small enough that “inside” has a useful business meaning. The fix supplies the root check; configuration decides what that root contains.
The regression matrix needs positive and negative controls. A normal child must upload. A supported internal link should follow documented behavior. An external absolute path, a sibling-prefix directory, nested traversal, an external link, and a different drive must stop before open(). Negative tests alone can hide compatibility damage. Positive tests alone cannot prove the security property. Both results belong in release acceptance.
Link fixtures require platform-specific care. Windows CI may need privileges to create a symbolic link, and a skipped test cannot be presented as full coverage. A junction or supported reparse fixture can exercise relevant behavior, with limitations recorded. Linux containers should verify that the external target is genuinely outside the root or on a separate mount. A fixture accidentally created under the base proves nothing.
Case and Unicode equivalence also vary by filesystem. The implementation should rely on pathlib and actual temporary directories, avoiding a homemade lowercase or encoding transform. Each supported platform runs its own path matrix. Untested combinations remain explicit limits; a POSIX pass does not automatically establish Windows service behavior.
3.3 Once cwd becomes the security root, launch configuration enters the threat model
validate_safe_path() defaults its base to Path.cwd(). That choice preserves existing workflows without requiring a new setting, but it changes cwd from a convenience into authorization configuration. Starting the service in a filesystem root, a credential-filled home, or a parent of several repositories produces a large allowed tree even with the patch active.
A systemd unit should set WorkingDirectory to a dedicated artifact directory and run an account that reads only the needed outputs. A container needs an explicit WORKDIR and narrow mounts. Placing secrets, a control socket, or a full host tree under the allowed root defeats the practical goal while satisfying the code check. A desktop launcher should create and select a predictable staging directory instead of inheriting whichever folder opened the application.
Windows services require acceptance under the real service identity. The current directory, drive mappings, and profile visible from an interactive PowerShell session can differ from Service Control Manager startup. Junctions, reparse points, and UNC access need tests on the actual platform. Startup telemetry can record resolved root, package build, and process identity without dumping the directory's contents or environment values.
Canonicalization and open() remain two filesystem operations. If an untrusted local principal can replace a link or path component between them, a race may exist. The public fix addresses remote parameter escape; it does not claim to solve every hostile-local-filesystem scenario. The artifact root and its parents should be owned by the service, with generated outputs copied in through a controlled producer.
The resulting contract has four parts. Code guarantees that the resolved target does not leave the base. Launch configuration makes the base narrow. Host permissions define readable objects inside it. Directory ownership keeps the check-to-open assumption stable. Each part can be tested separately, so a failure can be assigned to implementation, deployment, OS policy, or local ownership.
4 Untrusted content lit the fuse; the server decided how far it could burn
The advisory begins with Jira content, so prompt injection naturally attracts attention. It matters: a model can carry instructions from a business object into planning and invoke a high-impact tool with parameters that the user never examined line by line. Prompt injection explains how the request can be activated. The filesystem defect explains what that activated request can reach. Those are separate control questions, and a useful assessment measures them separately.
A stronger prompt, a different model, or an approval dialog may reduce activation probability without changing server authority. Another client, a direct tool call, a stolen session, or internal automation can still send the same path. Once the server enforces containment on every access, a future external selection stops in the same place regardless of the model. Client controls reduce dangerous intent and improve human understanding; server controls cap the consequences of any intent.
4.1 The journey from Jira text to Confluence attachment crosses four trust domains
The first domain is content. Issues, comments, and descriptions can be written by employees, outside collaborators, email automation, or other integrations. A client retrieves them as task context. Their presence in a project system is not an instruction audit. Object ID, author, permissions, and revision history should travel with the text so the agent and later investigators can distinguish “available to read” from “authorized to direct tools.”
The second domain is planning. A model combines system instructions, user intent, retrieved content, and exposed tools into a call. Model family, client implementation, context ordering, and confirmation policy all affect whether the advisory scenario reproduces. The published demonstration establishes a practical path. It does not imply that every deployment will automatically produce the same call. Each organization still measures its own activation conditions.
The third domain is the tool protocol. The client sends a structured invocation to mcp-atlassian with a content ID and path. Once that message exists, the downstream server no longer knows which Jira sentence influenced selection unless the client preserves provenance. A log containing only “upload_attachment succeeded” loses the crucial relationship between untrusted source and resource argument.
The fourth domain combines data and publication. The server opens a host object, and the integration identity creates a persistent remote attachment. Each action enters a different audit system with different object names and clocks. The result can then be indexed, announced, downloaded, exported, and backed up. One planning event has crossed content governance, tool governance, host data, and collaboration access control.
Defensive design leaves minimum sufficient evidence at every crossing: stable source object and revision, user and model session, tool name, policy decision, root-relative file object, destination content, and attachment ID. It need not record secret bytes or duplicate an entire prompt. Stable keys are enough to prove which content preceded which call and which persistent object the call produced.
The client should preserve source object ID, author, and trust class as structured context, then show the root-relative artifact, destination product and page, publishing identity, and influencing source before execution. Policy can require stronger approval or disable automatic writes when untrusted material participates. An external candidate still stops at the server instead of being offered for an easy misclick. The client explains intent while the server limits consequences, and neither guarantee depends on a particular model or interface.
4.2 Exposure is the intersection of readable host data and visible remote scope
Asset assessment begins with the running process. Record package version, Python environment, launch command, cwd, OS account, image, mounts, secret injection, enabled tools, and client configuration. Desktop instances, shared services, CI jobs, and development containers often use different identities and roots. A result from one laboratory process cannot represent every deployment. Each instance needs its own readable-root and remote-identity pair.
Readable objects commonly include source, build outputs, dotenv files, cloud credential caches, SSH material, package tokens, application configuration, logs, and mounted service-account tokens. Linux /proc/self/environ can expose the process launch environment, though actual contents depend on deployment and platform. Scope should test what the real service identity can read. A generic sensitive-path checklist is a prompt for discovery, not evidence of local exposure.
The remote side enumerates Confluence spaces and page permissions available to the integration identity, including guests and inherited groups. Because the fix also protects the Jira attachment path, deployments that enable Jira tools should include projects, issue security, and attachment access. The advisory's Confluence example is one destination. Configuration and caller-controlled target values determine the actual remote set.
Content lifecycle changes severity. A token seen by one administrator may require immediate revocation. Source copied to a broad space can create commercial impact that depends on repository exposure. Versions, exports, search indexing, mail notifications, and backups mean a visible deletion cannot erase every copy at once. Prioritization combines data sensitivity, audience, revocability, and retention.
The preconditions should be explicit. A caller or contaminated content must influence the invocation and path. The server must run a release below 0.22.0. Its process must be able to read the target. Its remote identity must be able to create the attachment. If any condition fails, this particular route stops. “Uses MCP” is not sufficient evidence of exposure, and “runs an affected package” is not sufficient evidence that data left.
Instances can be ranked from their measured combination. A shared entry point, broad cwd, sensitive mounts, long-lived remote token, guest-visible spaces, and weak confirmation raise priority. A single-user process with a dedicated empty staging directory, no attachment tool, or a read-only remote identity can be handled differently. Labels such as “development” or “internal” carry no automatic reduction without supporting configuration.
4.3 A safe reproduction needs two markers and a negative assertion
Validation does not require a credential file or operating-system secret. In an isolated temporary layout, create an allowed root and place one harmless marker inside it. Put a second harmless marker in a sibling directory. An affected build may accept both readable paths. A fixed build must upload only the internal marker and reject the external one before local open and remote request. Both files contain obvious test strings and the destination is a dedicated test page.
A traversal case begins inside the root and uses parent components to reach the sibling marker. An absolute case names that same object directly. A link case places a symlink in the root whose target is the sibling. All three must produce the same policy outcome. A second link whose target remains inside verifies compatibility. The record includes candidate class, resolved relation, direct-helper reachability, outbound request, and attachment ID.
The location of failure is the critical assertion. If the server reads the file and later fails on remote permissions, local path control is still absent. If it creates an empty attachment and then rolls back, remote audit noise remains. The integration test mocks or observes _upload_attachment_direct() and proves that an external candidate never reaches it. Atlassian should contain no object for the rejected request.
An end-to-end agent test forms a second layer. Place an explicit test instruction in a controlled Jira project and run the client under supervision. Observe provenance, confirmation, server rejection, and resulting audit. This experiment measures activation controls. The marker suite measures server authority. Keeping their results separate prevents a model update from masking a filesystem regression.
Normal business behavior belongs in the same acceptance run. Upload a document and an image from staging, exercise a batch, comment, minor edit, and same-name version, and confirm useful error messages. A security fix is deployable when it reliably rejects external objects while preserving the legitimate artifact path. The invariant is concrete: a pathname not accepted by the workspace resolver can never produce a remote attachment.
The test identity should write only to a dedicated page that is excluded from production notifications, search automation, and exports. Every created object is recorded and removed. Marker contents never contain a real secret. If an old build accepts the negative case, the result remains limited to organization-owned test data and still yields complete evidence for comparison.
Failures are classified before a verdict. An external marker that uploads is a security failure. An internal marker that cannot be published because page permission changed is a remote configuration failure. A link that could not be created is a fixture failure. This classification prevents network noise from concealing a path-rule regression and prevents ordinary service trouble from being reported as successful exploitation.
5 The fix resolves the real target before deciding whether it may be opened
Commit b041733473f95119dd539542a43c280737a8e460 does not replace the attachment stack. It connects the existing shared path validator to the Confluence and Jira local-upload entry points. The change is compact, but its order is decisive: convert a candidate into a canonical object relative to a trusted base, prove membership, and only then allow ordinary upload logic to create a handle.
The utility, call sites, and tests must be read together. The utility supplies the algorithm. The call sites prove that real sinks use it. The tests preserve behavior against future changes. Seeing a function called validate_safe_path() is not enough if attachment code bypasses it. Seeing a call is also insufficient without verifying the base, resolution, comparison, and returned object.
5.1 validate_safe_path() places base and candidate in one coordinate system
The validator lives in src/mcp_atlassian/utils/io.py. It accepts a candidate and an optional base_dir; without an explicit base it uses Path.cwd(). The function expands and resolves the base first. A relative candidate is joined to that base. An absolute candidate retains its own origin. Both then pass through platform path resolution before comparison.
The candidate is resolved with strict=False. This allows a canonical location to be computed even if the final object is missing. Later upload logic still owns existence and readability. The security property does not depend on using strict existence at this stage. It depends on comparing the resolved target and preventing an external candidate from entering file I/O.
Membership is decided with resolved_path.is_relative_to(resolved_base). Path components, not textual prefix, determine the ancestor relationship. Failure raises an exception. Success returns the resolved path. The attachment code replaces the original input with that return value, so basename selection, existence handling, and open() all operate on the same object that passed policy.
Returning the canonical object avoids the pattern “check one string and open another.” A boolean-only guard might let downstream code reuse the original relative path, and a cwd change or second transform could alter its target. The fixed return value becomes the sole input to later I/O. Tests can assert exactly which path the direct helper receives.
The optional base makes narrower integrations possible. The current attachment call sites do not provide a custom value, so runtime cwd is effective policy. A wrapper that introduces a configurable upload directory must apply it consistently to Confluence single and batch paths, Jira, and any new entry. Missing configuration, instance separation, and directory changes all need regression coverage.
Property-oriented tests can strengthen fixed examples. Every accepted candidate must resolve relative to the same base. Every known external object must remain rejected through absolute, relative, or link representations. Examples protect known regressions; the invariant constrains future refactors and new platform syntax.
5.2 Each product's upload_attachment() entry is a sufficiently narrow control point
AttachmentsMixin.upload_attachment() now invokes validate_safe_path(file_path), converts the result for existing helpers, derives the basename, and calls the direct uploader. Rejection occurs before a handle or HTTP request. This location preserves a business-level error and covers calls through FastMCP, fetchers, batch conveniences, and direct library use.
The Jira attachment helper uses the same utility. That detail matters because the advisory demonstrates Confluence while the root cause belongs to local file selection on the server. Protecting only the demonstrated product would leave another tool with equivalent host authority. A shared validator gives both products the same minimum local-filesystem contract.
A later control point would be too late. If code opens or buffers the file before checking, sensitive bytes can enter memory, logs, retries, or temporary objects. A remote API cannot know the host base. An earlier wrapper-only control point is too narrow because other callers can reach the mixin. The selected location sees the complete candidate and sits immediately before every relevant I/O sink.
The base64 branch needs different audit semantics. It does not select a server filesystem object, so it should not be forced through a temporary path merely to reuse the validator. It needs caller authorization, size limits, content-safe logging, and destination control. Both modes share “who may publish where”; local-path mode adds “which server object may supply bytes.”
Future reviews should walk in both directions. Starting at public tools finds registered entry points. Starting at open(..., "rb"), multipart file parts, and product attachment helpers finds sinks and library callers. Searching only validator references can miss a newly created unguarded sink. Searching only tool names can miss a direct fetcher user.
Dynamic tracing can confirm the static map. A test probe at the direct helper or file-open layer triggers every public attachment operation and checks for a preceding policy event. The call graph states what should happen; runtime coverage states which conditional branches and wrappers were exercised. Together they reveal omissions caused by registration, configuration, or reflection.
5.3 Version 0.22.0 is the upstream floor; downstream equivalence needs commit evidence
Upstream identifies 0.22.0 as the first fixed release, and the commit remains present in v0.22.0, v0.22.1, and later tags. Direct consumers can use 0.22.0 as the minimum. Distributions, internal forks, bundled desktop applications, and vendor images may use unrelated version strings. They must identify the commit or prove equivalent resolution, call-site coverage, and tests.
Acceptance begins with the running process. Record package-manager evidence, loaded module location, image or application digest, and restart time. Inspect that both Confluence and Jira paths call the validator. A hot-reload assumption is not enough: old Python workers, desktop background processes, or un-recreated containers can continue serving the previous module.
Upstream tests cover ordinary children, internal absolute paths, external absolute paths, parent traversal, and nested traversal, while attachment tests assert the guard precedes the direct helper. Downstream suites add the platforms and launch shapes they support: links, sibling prefixes, Windows drives and junctions, container mounts, batch tools, and the cwd selected by the real launcher.
The patch cannot make a broad root small. If cwd contains source, outputs, dotenv, credentials, and control sockets, all readable objects inside still satisfy containment. Upgrade work should create a dedicated staging directory, move only publishable artifacts into it, exclude secrets and sockets, and execute the marker suite as the actual service identity.
Already-created attachments also survive the code update. The new release prevents fresh external selection; historical review still needs tool records, host events, and remote objects. Put version installation, old-worker exit, and the first successful external-path rejection on one timeline. If those moments are uncertain, extend the possible window until fixed code and a narrow root are both proven.
SBOM and runtime inventory should capture every packaging form. A desktop product may bundle the module. A container may install it during build. An internal tool may pin a Git revision. Starting from the running module and mapping back to image and build records is more reliable than scanning only top-level requirement files.
A backport record includes source commit, changed call sites, test output, rebuilt digest, and the active root. Copying the utility while omitting Jira, batch, or a wrapper is not equivalent. Release notes should identify protected entry points and base semantics so asset owners can verify behavior without guessing from a private version number.
6 Host and cloud each preserve half the event until correlation joins them
The disclosure route crosses a content system, agent, tool server, filesystem, network, and Atlassian. Every source sees one segment. Jira sees text. The client sees a call. The host sees a read. A proxy sees HTTPS. Confluence sees an attachment. Investigation quality comes from making those records explain one another around the same object, process, identity, and corrected time.
A tool request and attachment ID are often the strongest anchors. The request is closest to argument provenance. The attachment is the persistent result. Process, container, destination host, byte count, and time can connect file-open and network evidence between them. When no end-to-end correlation ID exists, the case creates one and labels every join as direct observation, strong association, or inference.
6.1 A reproducible timeline needs at least seven object classes
The first class is source content: stable Jira or Confluence ID, revision, author, modification time, and client retrieval time. Original content is preserved through the incident evidence process, with only necessary excerpts in broad reports. The goal is to show what the session could see at the moment of planning, without copying a sensitive or manipulative instruction into every ticket.
The second class is session and tool invocation: user, client instance, model session, tool name, request ID, destination content, path or redacted relation, confirmation result, response, and timestamp. A raw pathname may reveal names and project structure. Fixed deployments can record workspace ID, inside-or-outside outcome, and policy version so aggregations do not need the original external value.
The third class is the host object: PID, process start, loaded build, cwd, container ID, path, inode or Windows file identity, open mode, and bytes read. The object may later change or disappear. Hashing follows approved evidence handling. A process pseudo-file requires process identity and collection time because its value is not a static disk object.
The remaining classes are outbound request, remote attachment, viewer or download activity, and later credential use. Network data supplies destination, connection time, bytes, and proxy identity. Attachment data supplies page, version, and permission. Access data indicates propagation. Authentication data tests whether disclosed material led to another state change. Each source carries its retention limit and susceptibility to modification.
The completed timeline answers four questions: who or what initiated the call, which host object the server opened, which remote attachment received the bytes, and who could access the result. An unanswered question stays visible and becomes a collection task. Keeping blanks intact allows conclusions to improve with new evidence without revising an earlier narrative that claimed too much.
Every source needs a coverage statement. A container may lose application logs on restart. A host audit rule may watch only selected directories. An Atlassian plan may not expose each download. A proxy may retain host and byte count but no path. A negative result is meaningful only when an independent source completely covers the window and was difficult for the actor to modify.
Clock correction is preserved as evidence. Record original formats, NTP state, known requests used to estimate skew, and corrected UTC. When precision is limited, use a range. In a busy upload service, process lifetime, content ID, attachment size, and destination help distinguish calls that share the same minute. False precision is less useful than an honest interval with strong object joins.
6.2 Remote remediation must cover versions, viewers, and derived copies
Before changing a suspicious attachment, preserve ID, content ID, filename, versions, creator, timestamps, size, page permissions, and available audit. If exposure is ongoing, restrict access immediately while preserving evidence through an approved forensic route. Downloading the content simply to inspect it can enlarge the audience. A minimal authorized group handles sensitive bytes.
The remote lifecycle is reviewed item by item. Can an older attachment version still be retrieved? Does page history or export contain the data? Did search create an index entry? Did notification or automation copy it elsewhere? When will backups expire? Removing the visible current object cannot answer those questions. Every copy receives an owner, state, verification method, and expected disposal date.
If the object contains a token, private key, password, or session material, revocation can proceed before the viewer list is complete. Replace it from a trusted endpoint, invalidate derived sessions, and monitor use of the old value. The response action is itself a system change, so its time and operator belong in the case record and can be separated from attacker behavior.
Source, configuration, and regulated data need different owners. A repository secret may also live in history and build cache. A configuration can reveal internal topology. Customer or employee data can trigger notification analysis. The technical fact “this file became an attachment” must be translated into content-specific actions. One deletion ticket cannot make all of those decisions.
Remote administrators record every restriction, version deletion, export, and evidence action with ticket, operator, UTC, and before-and-after state. If an object later appears missing, investigators can tell whether response removed it or an actor attempted cleanup. The record also shows how quickly the organization reduced continued access after discovery.
Closure means the host rejects external paths, known remote copies are controlled, exposed credentials are invalid, and backup constraints are documented. Anything incomplete remains a dated, owned residual risk. “The page no longer shows the attachment” is one observable state, not a complete disclosure-lifecycle conclusion.
Notification scope belongs to data owners and counsel. Technical teams supply object identity, content class, viewers, and retention. If content is not yet approved for inspection, path, producer, size, and destination can support an initial conservative classification. A small authorized team then confirms what is necessary without turning investigation into another uncontrolled distribution.
6.3 Response order stops new disclosure while preserving the state that already existed
- Locate all instances and preserve volatile evidence. Inventory desktop, container, shared-service, and CI versions, processes, cwd, mounts, tools, and remote identities. Export client and server logs, process information, relevant proxy data, and Atlassian metadata. If operations permit, disable local attachment tools or remove remote write permission while keeping the process and evidence available.
- Upgrade and restart. Install 0.22.0 or later, verify the loaded fix, and ensure every old worker exits. Start in a dedicated artifact root and run internal, absolute-external, traversal, and link markers. Every external case must stop before the direct helper and produce no remote ID. The internal control must still upload successfully.
- Investigate history and constrain remote objects. End the vulnerable window when the last old process exits and the fixed path check passes. Search backward through the deployment and available retention. Join source content, tool calls, file-open, outbound traffic, and attachments. Restrict a suspected sensitive object after preserving metadata, then involve its data owner.
- Reduce long-term authority. Give attachments a service-owned staging directory, remove dotenv files, credentials, sockets, and unrelated repositories, and separate read-only from write-capable profiles. The client shows provenance and target details. The server applies path policy on every access. These controls reduce accidental activation and enforce a hard maximum.
- Keep acceptance repeatable. Record the resolved root and package build at startup, use a focused path matrix to check updated behavior, monitor policy rejections and unusual attachment creation, and periodically review Atlassian identity scope. Roots, mounts, wrappers, and tools change over time, so later changes must prove that the allowed tree has not silently expanded.
If immediate upgrade is impossible, a temporary control can remove the tool from the profile, revoke remote write, move the process into an empty staging root, or block the operation at an outer layer. The choice depends on business use. Every mitigation has a test and expiry. It does not resolve historical attachments and must not be recorded as the permanent code fix.
Communications differ by deployment. A desktop user needs a clear upgrade, restart, and working-directory guide. A shared service needs a change window, evidence handling, credential rotation, and caller notification. Both retain the GHSA, fixed floor, and harmless marker test. Neither publishes real sensitive paths or an unconfirmed loss list to a broad audience.
7 A managed artifact makes the capability evolution and operational closure concrete
The visible defect is one missing directory limit. The deeper pattern is the composition of three legitimate powers: untrusted content can influence planning, a tool can use host authority to obtain bytes, and a durable cloud identity can publish them. The fixed invariant remains concise: a candidate's resolved target must be beneath the resolved workspace root before any handle exists.
The next useful step is not another checklist around a free-form path. It is to represent the thing being published as a short-lived capability whose origin, owner, scope, and destination can be checked again at the instant of use. That evolution keeps the compatibility path available while giving new workflows an object that carries the missing authorization state.
Return to the opening attachment at the moment the model chooses it. With a free-form path, the server receives only an address and must rediscover every relevant fact while the file can still be replaced, redirected through a link, or confused with another tenant's output. With a managed artifact, the choice is already a sealed statement: this workflow produced these bytes, under this root, for this class of destination, and the statement expires. The attachment action consumes that statement; it does not reinterpret an address as permission.
7.1 Artifact identity replaces a caller-authored host address
Producer tools place outputs in service-owned staging and register an object ID with a root-relative path, size, type, creator, and expiry. The attachment tool accepts that ID and revalidates the object at use time before opening it. During migration, a validated file_path can be converted into the same internal object, so compatibility does not create a second security model.
Registration should be a state transition, not a database label pasted onto an existing path. A producer writes into a private temporary location while the service measures the stream, determines the permitted media type, and calculates a digest. Only an atomic move into staging changes the object from pending to sealed. The immutable manifest then records the final size and digest beside the creator and expiry; a partial write or late mutation cannot quietly inherit the sealed object's authority.
The capability is bound to the session or workflow that created it and to approved destinations. A cross-page, cross-product, or cross-tenant publication requires a new authorization instead of inheriting the first one. Admission has one non-negotiable caveat: no other tool may copy an arbitrary host file into staging, because that would merely move the unbounded choice one step earlier.
The authorization answer is therefore a tuple, not a Boolean attached to a filename: publishing identity, operation, artifact ID, destination tenant and container, and validity window must agree. The confirmation shown to a person can be derived from the same tuple—display name, byte size, provenance, destination, and acting cloud identity—so the screen and the server enforce the same decision. Replaying the object ID against a different page or issue fails because the destination changed, even though the bytes did not.
Managed objects also have revocation, expiry, and retry semantics. Completion, cancellation, or timeout invalidates the capability; retries may reuse it only while it remains valid. If one approved object genuinely sits outside staging, a controlled job copies that object into an isolated area while recording origin, hash, approval, and expiry. The exception widens one object, not cwd or the visible host tree for every later call.
Use-time validation must also close the interval between checking a pathname and opening it. The service can open a handle inside its controlled directory, confirm that the handle still represents the sealed object, and stream from that handle while enforcing the recorded size; environments that cannot make those guarantees copy once into isolated staging and publish only the copy. This is why resolving a path once is a necessary repair for the current code but not the final shape of a transferable capability.
Retries become easier to reason about when the upload has an idempotency record keyed by artifact, destination, and operation. A network timeout can resume or query the same attempt without minting broader authority, while a second destination requires a new decision. Events for issue, seal, authorize, open, outbound request, remote attachment ID, consume, revoke, and expire form one compact history. That history contains no reusable file bytes, yet it can join the host-side choice to the object that Atlassian ultimately stored.
7.2 The opening attachment closes only when possibility and occurrence remain separate
Before the fix, a successful upload proved that the process could read an object, the integration could write a page, and Atlassian accepted the multipart request. After the fix, the same success gains a verifiable premise: the canonical source is beneath a narrow staging root and the direct helper receives that validated object. A managed artifact makes the premise explicit instead of reconstructing it from a string after the event.
That distinction changes the investigation queue. A vulnerable version with a broad mount is remediated immediately because the reachable set is large, but it remains an exposure finding until runtime evidence identifies a candidate. A tool invocation naming an external path raises the case further, yet it still does not prove that the open or upload succeeded. Only the correlated sequence—resolved object, successful read, multipart request, remote response, and attachment identifier—supports a disclosure conclusion. Missing telemetry reduces confidence; it does not turn an assumption into an event.
Closure likewise has an independent result on every side. The host runs only fixed workers and rejects external candidates before the direct helper. The remote system controls known attachment versions, viewers, derived copies, and backup timelines. Identity systems invalidate exposed material. The client preserves provenance and presents the artifact, destination, and publishing identity for confirmation. When those results converge, the ordinary attachment from the opening can finally answer both where the bytes went and why they were entitled to leave the host.
Research record
8Evidence, objects, and sources
The material below preserves the identifiers and references used in this report.
8.1Research objects
Products, actors, techniques, affected objects, and control points discussed in the report.
mcp-atlassian local-file read and remote attachment upload
Affected range; first fixed in 0.22.0
FastMCP attachment entry point
Local open and multipart control point
Canonical workspace containment
Fix included in v0.22.0
8.2Event chronology
- Security advisory published
GHSA-g5r6-gv6m-f5jv publishes affected scope, CVSS 7.7, and the end-to-end prompt-injection demonstration.
- Upstream fix committed
The upstream release wires its shared safe-path check into both products' attachment flows.
- Version 0.22.0 released
Upstream identifies 0.22.0 as the first fixed release.
- SOSEC completes source reconstruction
SOSEC pins the affected call path, patch semantics, deployment conditions, evidence correlations, and response order.
8.3Sources and material
- mcp-atlassian GHSA-g5r6-gv6m-f5jv advisoryhttps://github.com/sooperset/mcp-atlassian/security/advisories/GHSA-g5r6-gv6m-f5jv
- Fix commit b041733https://github.com/sooperset/mcp-atlassian/commit/b041733473f95119dd539542a43c280737a8e460
- Historical commit 52b9b09 introducing validate_safe_pathhttps://github.com/sooperset/mcp-atlassian/commit/52b9b09
- mcp-atlassian v0.22.0 releasehttps://github.com/sooperset/mcp-atlassian/releases/tag/v0.22.0
- v0.22.0 Confluence FastMCP tool definitionhttps://github.com/sooperset/mcp-atlassian/blob/v0.22.0/src/mcp_atlassian/servers/confluence.py
- Confluence AttachmentsMixinhttps://github.com/sooperset/mcp-atlassian/blob/v0.22.0/src/mcp_atlassian/confluence/attachments.py
- Jira attachment helperhttps://github.com/sooperset/mcp-atlassian/blob/v0.22.0/src/mcp_atlassian/jira/attachments.py
- validate_safe_path implementationhttps://github.com/sooperset/mcp-atlassian/blob/v0.22.0/src/mcp_atlassian/utils/io.py
- Path-containment regression testshttps://github.com/sooperset/mcp-atlassian/blob/v0.22.0/tests/unit/utils/test_io.py
- Model Context Protocol tools specificationhttps://modelcontextprotocol.io/specification/2025-06-18/server/tools
- Model Context Protocol security best practiceshttps://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices
- Python pathlib Path.resolvehttps://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve
- Python pathlib PurePath.is_relative_tohttps://docs.python.org/3/library/pathlib.html#pathlib.PurePath.is_relative_to
- Atlassian Confluence content attachments APIhttps://developer.atlassian.com/cloud/confluence/rest/v1/api-group-content---attachments/
- Atlassian Jira issue attachments APIhttps://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-attachments/
- CWE-22 Improper Limitation of a Pathname to a Restricted Directoryhttps://cwe.mitre.org/data/definitions/22.html
- CWE-73 External Control of File Name or Pathhttps://cwe.mitre.org/data/definitions/73.html
- NIST SP 800-61 Rev. 3 Incident Response Recommendationshttps://csrc.nist.gov/pubs/sp/800/61/r3/final