Intelligence
AEW-040

Destination confinement assumed from an unsafe join, not enforced by one

A privileged write's destination is built by joining an authorized root directory with a fragment of caller influenced content, using a function that performs no containment check, while a second function in the identical codebase performs the identical join safely and is already the default at other call sites. The unchecked join's own output, not the declared root, decides where the write actually lands.

Status: publishedExecution AuthorityEnvironment Boundaries

Description

Some systems that must recombine an authorized root directory with a piece of caller influenced content, an object key, an archive entry name, a resolved reference, do so correctly: a containment checked join cleans the result and refuses to return a path outside the root. This weakness appears when a codebase holds exactly that correct function, proven and already in production use at other call sites, and a separate, security relevant call site reaches for a plainer sibling instead, one that performs the identical join with no containment check at all. Both the destination root and the joining operation look, from a distance, like a solved problem: the root is genuinely bounded by its own upstream caller, and a working, tested containment mechanism genuinely exists in the same file. The gap is narrower and easier to miss than either a wholesale missing check or a check that runs and fails: the correct function was simply never called at this one site, and the caller influenced fragment's own content, not the destination root the operation was authorized against, ends up deciding where the write lands. It differs from an operation's authority being silently equated with authority over whatever it resolves against (AEW-013), whose own known example turns on a target identifying argument reinterpreted in shape, a predicate rather than a scalar, within one request; here the argument is an ordinary path fragment, its shape never in question, and the failure is which of two available functions processes it. It differs from a container's authority being equated with authority over a shared object it references (AEW-014), which concerns a mutation persisting to other actors through a reference resolved later, with no equivalent persistence or reference indirection here. It differs from a failed resource resolution inheriting the trust of the apparent path (AEW-017), where a canonicalization check exists, runs, and fails, and the failure is converted into a pass; here the join does not fail, it simply never checks at all, because the function that would have checked was not the one called. It differs most from a destination confined while its paired source is not (AEW-018), whose own gap is one side of a two sided operation carrying no location check whatsoever; here there is no unchecked side by design, the destination root is confined correctly and a genuine containment mechanism for the join itself exists in the same file, and the vulnerability is that one call site quietly bypasses it by calling its unchecked sibling instead.

The authority gap

The authority presented is authority to invoke a bounded operation whose destination root is genuinely confined by its own upstream caller. The authority required is that the specific path the operation's own join function resolves against remains inside that same root, which an unsafe join silently assumes rather than enforces, even though the codebase's own containment checked alternative was already available, and already the default at other call sites, to call instead.

Failure conditions

  • A privileged write operation's destination is resolved by joining an already authorized root directory with a fragment of caller influenced content, such as an object key, an archive entry name or an equivalent identifier.
  • The join is performed by a function that does not verify the resulting path remains inside the root, even though a distinct function performing the identical join with that verification already exists in the same codebase and is already called elsewhere.
  • The caller influenced fragment can carry path traversal segments the join's own unsafe implementation neither strips nor rejects.
  • Nothing downstream of the join independently re-verifies the resolved path against the authorized root before the write executes.

Consequences shown by the evidence

  • A caller restricted to one narrow, bounded operation or document redirects a privileged write to an arbitrary path the operation's own destination root never authorized.
  • A codebase's own working containment check, proven correct and already deployed at other call sites, provides no protection at the one site that never calls it.

Detection signals

  • Two functions in the same package perform structurally the same path join, one checked, one not, and a security relevant call site uses the unchecked one.
  • A fix replaces one function call with a differently named sibling already defined in the same file, with no new validation logic written alongside it.
  • A caller influenced identifier, listed or resolved from an external source rather than typed directly by the caller, reaches a path join with no separate traversal check of its own.

Known examples

  • Amazon's own amazon-ssm-agent implements aws:downloadContent, an SSM document an administrator can restrict a principal's ssm:SendCommand permission to through an IAM condition, narrowing that principal to one bounded operation, download a caller named S3 object into a caller named destination directory. This record independently cloned aws/amazon-ssm-agent and confirmed, by direct reading of agent/plugins/downloadcontent/s3resource/s3resource.go at the affected tag, that the local write path for a downloaded S3 object is built by joining the correctly bounded destination directory with the portion of the real S3 object key beyond the caller's configured prefix, using fileutil.BuildPath, a function this record confirmed performs a plain filepath.Join with no check that the result stays inside the root. A second function in the identical file, fileutil.BuildSafePath, performs the same join with exactly that check, returning the unmodified root the instant a joined element would escape it, and this record independently confirmed BuildSafePath was already called, at the same affected version, by five other plugins in the same codebase. An S3 object key can carry path traversal segments with nothing in S3 itself to prevent it; joined through the unchecked function, those segments carried a downloaded file's write path outside the intended download directory, written with whatever privileges the agent process itself holds, root on Linux per the advisory's own stated consequence. Formally assigned CVE-2026-81849 on 28 August 2026, CVSS 4.0 base score 8.7, High. The fix, shipped in version 3.3.4515.0 and confirmed by this record as replacing the single vulnerable call, and a second, adjacent one in the plugin's GitHub git-resource directory download path, with the already existing fileutil.BuildSafePath, required no new containment logic, only calling the function the rest of the codebase already trusted.

Protocol evidence

Published authority architectures whose artifacts bear on the requirement this weakness names: which satisfy it, which expose a way past it, and which leave it unanswered.

  • No protocol change

    Agent Control Standard (ACS)

    OWASP GenAI Security Project, originally Zenity

    ACS's own conformance document addresses whether a consequential action reaches an independent mediation point at all; amazon-ssm-agent's own aws:downloadContent mechanism is not that kind of gap. The document boundary itself was correctly enforced, an IAM condition genuinely restricted the principal to one named document, and this weakness's own evidence is a codebase level implementation defect inside that already authorized operation, one call site joining a destination directory with caller influenced content through an unchecked function instead of the containment checked sibling already defined in the same file and already called elsewhere in the same codebase. Nothing in the material available to this session states or implies a requirement, of ACS's or of any other protocol draft this registry currently holds, that would have been engaged differently had it applied here: the fix is calling an existing, already correct function, not adopting a new mediation, delegation or containment requirement this dataset's protocol material does not already cover in the abstract. Recorded as no change rather than supporting or missing evidence for any specific protocol requirement.

    View protocol evidence