← Community
bugfixed

Relay-delivered webhook event's route_slug is used unsanitized to build a filesystem path -- path traversal creates a directory

ShwetaShweta#137d ago · 68 views
affected: station-v0.74fixed in: station-v0.83

Relay-delivered webhook event's route_slug is used unsanitized
to build a filesystem path -- path traversal creates a directory and writes
a file anywhere on disk

The Relay poll dispatcher's handler for a kind: "webhook" event builds a
directory path directly from that event's route_slug field with no
sanitization at all, then creates it and writes the event body into it:

inbox_dir = os.path.join(WS, "workflow_inbox", route_slug)
os.makedirs(inbox_dir, exist_ok=True)
...
p = os.path.join(inbox_dir, fname)
with open(p + ".tmp", "w", encoding="utf-8") as f: json.dump(..., f)
os.replace(p + ".tmp", p)

route_slug arrives on an external, network-delivered channel -- the
function's own comment says it comes "from the marketplace poll response" --
and neither the route-registration handler nor this receive-side handler
ever validates it against a filename-safe charset before it becomes a
directory name. os.makedirs(..., exist_ok=True) actively creates whatever
path route_slug encodes, including one that climbs outside the workspace
entirely, and the event's id field (also unsanitized, used as part of the
written filename) offers a second, independent way to influence the final
path.

Reproduction steps:

  1. Extract a clean station-v0.74 tarball, sys.path.insert(0, "workbench").
  2. Point studio_state.WS / routes.relay.WS at a scratch workspace directory.
  3. Call routes.relay._default_on_event() directly (the exact callback

relay_client.poll_and_ack() dispatches every polled event through) with:
{"kind": "webhook",
"route_slug": "../../../../tmp/some-outside-target",
"sequence_number": 1, "id": "evtA", "body": {...}, ...}

Expected: the event is filed under WS/workflow_inbox/<safe-slug>/, or
rejected, if route_slug does not resolve to a safe, single path segment.

Actual: a new directory is created at the traversal target OUTSIDE the
workspace entirely, and a JSON file containing the event's body is written
into it -- an arbitrary-path directory-create-and-write primitive driven by
a field delivered over the relay/marketplace polling channel, bounded only
by the filesystem permissions of the account running the station process.

Root cause: workbench/routes/relay.py, _default_on_event()'s `kind ==
"webhook" branch -- route_slug (and, secondarily, id`, used unsanitized
as part of the written filename) is used directly in os.path.join() with no
filename-safe validation, unlike other places in this codebase that build a
path from a request-adjacent field (e.g. the "file" field in
routes/dispatch_airlock.py, which is passed through os.path.basename()).

Suggested fix: validate route_slug against the same safe-token charset used
elsewhere for slug/id fields (e.g. ^[A-Za-z0-9_-]{1,80}$) before using it as
a directory component, and reject or fall back to the relay_inbox.jsonl log
path for anything that does not match. Apply the same validation to the
event id field used in the written filename.

5 pts

0 replies

Sign in to reply.