Skip to content

[security] fix(agent-route): reject unsafe forwarded attachments#2468

Open
Hinotoi-agent wants to merge 1 commit into
nanocoai:mainfrom
Hinotoi-agent:security/a2a-attachment-symlink-guard
Open

[security] fix(agent-route): reject unsafe forwarded attachments#2468
Hinotoi-agent wants to merge 1 commit into
nanocoai:mainfrom
Hinotoi-agent:security/a2a-attachment-symlink-guard

Conversation

@Hinotoi-agent
Copy link
Copy Markdown
Contributor

Summary

This PR hardens agent-to-agent attachment forwarding so the host does not follow container-controlled symlinks when copying files from a source agent outbox into a target agent inbox.

Before this change, forwardAttachedFiles(...) accepted safe-looking attachment basenames and then used fs.copyFileSync(src, dst). Because copyFileSync follows symlinks, a compromised or malicious agent could place a symlink in its writable outbox and ask the host to forward it as an attachment. The host would then copy the symlink target bytes into another agent's inbox if the host process could read the target.

The patch makes the source outbox and each forwarded file subject to host-side lstat/realpath checks before copying.

Security issues covered

Issue Impact Fix
Agent-to-agent attachment symlink follow Container-controlled outbox entries could make host-side forwarding copy host-readable files into another agent inbox Reject symlinked/non-regular/non-contained source attachments before copyFileSync

Before this PR

  • Agent-to-agent forwarding validated the requested attachment name with isSafeAttachmentName(filename).
  • The host then built src = path.join(sourceDir, filename) and copied it with fs.copyFileSync(src, dst).
  • A source attachment that was a symlink still passed the basename check.
  • The copy operation followed the symlink and copied the symlink target's bytes.

After this PR

  • The message outbox id is validated as a safe path segment before locating the source outbox.
  • The source outbox must be a real directory, not a symlink.
  • Each requested attachment must be a regular file, not a symlink or another special file type.
  • Each source attachment is resolved with realpathSync and must remain inside the resolved source outbox before it is copied.
  • Unsafe attachments are skipped instead of being forwarded.

Why this matters

Agent workspaces/outboxes are intentionally writable by the agent container, but host-side file forwarding runs outside that container boundary. A filename-only check is not sufficient when the directory entry itself is controlled by the container.

Without this guard, an agent that can write its outbox can turn an apparently benign attachment name such as safe-name.txt into a symlink to another host-readable path. The host then becomes the component that dereferences and copies the target file.

How this differs from #2001

This is the same trust-boundary family as #2001, but it is not the same code path.

#2001 hardened normal outbound attachment handling and cleanup in src/session-manager.ts, where the host reads container outbox files and removes message outbox directories. This PR hardens the separate agent-to-agent forwarding path in src/modules/agent-to-agent/agent-route.ts.

The distinction is:

Both paths need their own boundary checks because agent-to-agent forwarding reimplements the file-copy step instead of going through the already-hardened normal outbox reader.

Attack flow

  1. A compromised or malicious source agent writes an outbox directory for a message id it will send.
  2. Inside that outbox, it creates a safe-looking attachment name such as safe-name.txt.
  3. That directory entry is a symlink to a host-readable target outside the message outbox.
  4. The agent sends an agent-to-agent message whose JSON content includes files: ["safe-name.txt"].
  5. The host routes the message and forwards attachments from the source outbox to the target agent inbox.
  6. On vulnerable code, fs.copyFileSync follows the symlink and copies the target bytes into the target inbox.

Affected code

  • src/modules/agent-to-agent/agent-route.ts
    • forwardAttachedFiles(...)
  • src/modules/agent-to-agent/agent-route.test.ts
    • Added regression coverage for symlinked source attachments.

Root cause

  • The code treated isSafeAttachmentName(filename) as sufficient validation for a container-controlled source file.
  • Basename validation prevented path traversal in the string, but did not validate the filesystem object behind that basename.
  • The host copied the file with fs.copyFileSync, which follows symlinks.
  • The forwarding path lacked the lstat/realpath/containment checks used by related hardened outbox handling.

CVSS assessment

  • Issue: agent-to-agent attachment symlink follow during host-side forwarding
  • CVSS v3.1: 7.1 High
  • Vector: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
  • Rationale: exploitation requires the ability to control an agent outbox, but no user interaction is required once that control exists. The impact crosses the container/host trust boundary because the host process dereferences the source entry and can disclose host-readable file contents to another agent inbox.

Safe reproduction steps

A focused regression test can exercise the vulnerable behavior without using real secrets:

  1. Create a temporary host file containing sentinel bytes.
  2. Create a source agent outbox for an agent-to-agent message.
  3. Place a symlink in that outbox using a safe attachment basename, pointing to the sentinel file.
  4. Send an agent-to-agent message with files: ["safe-name.txt"].
  5. Inspect the target agent inbox attachments.

Expected vulnerable behavior

On vulnerable code, the target inbox receives an attachment whose contents are the sentinel bytes from the symlink target. That proves the host followed the source attachment symlink while forwarding.

After this patch, the symlinked attachment is skipped and no copied attachment contains the sentinel bytes.

Changes in this PR

  • Added a local path-containment helper for resolved outbox/file paths.
  • Validated source.messageId before using it as an outbox path segment.
  • Required the source message outbox to be a real directory and not a symlink.
  • Required each source attachment to be a regular file and not a symlink.
  • Resolved source files with realpathSync and required them to remain inside the resolved source outbox.
  • Added a regression test covering symlinked source attachments.

Files changed

Category Files What changed
Runtime hardening src/modules/agent-to-agent/agent-route.ts Added source outbox/file lstat checks, realpath containment, safe message id validation, and symlink/non-regular rejection
Regression tests src/modules/agent-to-agent/agent-route.test.ts Added coverage that symlinked source attachments are skipped and host bytes are not copied

Maintainer impact

  • Valid regular-file attachments continue to forward normally.
  • Unsafe or missing attachments continue to be skipped rather than aborting the whole message route.
  • The patch is intentionally scoped to agent-to-agent forwarding and does not change unrelated channel attachment behavior.
  • The source outbox validation mirrors the existing host/container boundary already used in adjacent outbox handling.

Fix rationale

The host should validate the filesystem object it is about to copy, not only the string used to name it. Rejecting symlinks and requiring the resolved source file to stay inside the resolved source outbox makes the security boundary explicit at the point where host-side copying occurs.

Type of change

  • Fix - bug fix or security fix to source code
  • Feature skill - adds a channel or integration (source code changes + SKILL.md)
  • Utility skill - adds a standalone tool (code files in .claude/skills/<name>/, no source changes)
  • Operational/container skill - adds a workflow or agent skill (SKILL.md only, no source changes)
  • Simplification - reduces or simplifies source code
  • Documentation - docs, README, or CONTRIBUTING changes only

Test plan

Local validation run:

pnpm exec vitest run src/modules/agent-to-agent/agent-route.test.ts -t "file forwarding"
pnpm exec vitest run src/modules/agent-to-agent/agent-route.test.ts
pnpm exec prettier --check src/modules/agent-to-agent/agent-route.ts src/modules/agent-to-agent/agent-route.test.ts
pnpm exec eslint src/modules/agent-to-agent/agent-route.ts src/modules/agent-to-agent/agent-route.test.ts
pnpm run typecheck
pnpm run test -- --run src/modules/agent-to-agent/agent-route.test.ts
git diff --check

Notes:

  • The focused agent-route tests pass locally.
  • TypeScript typecheck passes locally.
  • Diff whitespace check passes locally.
  • pnpm run lint was also attempted, but the repository-level lint script reported pre-existing warnings outside this patch. The touched-file ESLint command above passed.

Disclosure notes

This PR is intentionally bounded to the agent-to-agent attachment forwarding path. It does not claim to cover every host/container filesystem interaction in the project. It specifically closes the symlink-follow variant in forwardAttachedFiles(...) by validating the source outbox and source attachment filesystem objects immediately before host-side copying.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR: Fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant