security write-up · 2026
Path Traversal in a Self-Hosted Agent Dashboard
What I found, how I verified it, and what it taught me.
The short version
A self-hosted dashboard that controls an AI agent wrote files to disk using a path built from user input. Nothing checked that the path stayed inside the intended folder. Anyone who could reach the database could write files anywhere on the host machine. I found it, proved it with a working payload, patched it at two layers, and re-ran the attack to confirm the fix.
1. The problem
The project is a mission-control dashboard for a local AI agent: dispatch tasks, approve actions, browse the agent’s memory. The part I reviewed was the memory-wiki feature, which lets the agent save notes to disk.
The threat model is unusual. The database is the trust boundary: anything that can write to Postgres can create a task, and a task runs as a real agent with a real terminal. So I treated every input path as untrusted, including anything that could reach the database directly rather than through the web UI.
2. The environment
- Next.js 16 dashboard with a Postgres message bus
- A Node.js bridge that polls the database and shells out to the agent CLI
- The memory endpoint: POST /api/hermes/memory → bridge → writes markdown to a wiki folder
- An approval inbox where side-effecting requests wait for a human
3. The steps
- Read the bridge’s write function. One line stood out:
path.join(WIKI_DIR, e.path). The destination folder plus a path taken straight from the request, with no check that the result stayed inside WIKI_DIR. - Checked the API route. It passed the path field through untouched.
- Checked the approval flag. Memory writes were labeled
sideEffecting: false, so they skipped the human-approval inbox entirely. - Crafted a payload:
../../../../tmp/evil.md. If the check was missing, the file would land outside the wiki.
4. The finding
Arbitrary file write on the host machine, reachable through the approval-free path. On a machine running an agent, that is one step from code execution: write a cron job, drop a file into ~/.ssh/, overwrite a script the agent will run. I rated it high severity. Because of the database-is-trust-boundary design, it was reachable by anyone with write access to Postgres, not just through the web UI.
5. The fix and the lesson
I patched it twice, because the threat model says the database itself is untrusted input:
- At the API edge: reject any path containing “..” segments or absolute paths (HTTP 400).
- At the bridge: resolve the path and refuse anything outside the wiki directory.
Then I re-ran the attack from the database directly, bypassing the web layer. The bridge refused it: “refusing path outside wiki”. Legitimate writes still worked.
The lesson: sanitizing at the API is not enough when the database sits inside the trust boundary. The enforcement has to happen where the filesystem is touched. The approval inbox is also a UX layer, not a security control: the request declared its own risk level, and an attacker with database access could simply label their request safe.
6. Why this is SOC work
This is what I want to do: read code looking for the gap between how a system is supposed to work and how it actually behaves, prove the gap with a working exploit, fix it, and write it up so the next person does not make the same mistake. Secure code review, threat modeling, and verification discipline. The same loop as triaging a detection or validating a finding before it goes on the ticket.