> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentium.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Path & SSRF Hardening

> safeJoin, PathSecurityError, and allowedHosts for tool toolkits

# Path & SSRF Hardening

Agents that accept paths or URLs from the LLM (which got them from a user) are a classic source of security holes:

* **Path traversal:** the LLM helpfully passes `../../etc/passwd` to your `read_file` tool.
* **SSRF (Server-Side Request Forgery):** the LLM fetches `http://localhost:9200/_search` and exfiltrates your internal Elasticsearch index.
* **Null byte injection:** `legit.txt\0/etc/shadow` — some filesystems still trim at the null.
* **Control character injection:** ANSI escape sequences or carriage returns in paths.

v2.0 ships hardened defaults plus building blocks for your own tools.

## `safeJoin(base, rel)`

```typescript theme={null}
import { safeJoin, PathSecurityError } from "@agentium/core";

const full = safeJoin("/var/data", "users/2024.json");
// → "/var/data/users/2024.json"

safeJoin("/var/data", "../../etc/passwd");
// throws PathSecurityError("Path traversal blocked: ../../etc/passwd resolves outside /var/data")
```

### What it blocks

| Attack                 | Example input                                    | Behavior                                    |
| ---------------------- | ------------------------------------------------ | ------------------------------------------- |
| Parent traversal       | `"../../etc/passwd"`                             | throws PathSecurityError                    |
| Absolute path          | `"/etc/passwd"`                                  | throws PathSecurityError                    |
| Null byte              | `"ok\0.txt"`                                     | throws PathSecurityError                    |
| Control characters     | `"x\u0001.txt"`                                  | throws PathSecurityError                    |
| Prefix-matching escape | `safeJoin("/etc", "passwd")` from base `/etcfoo` | distinct directories enforced via PATH\_SEP |

### What it does NOT do

* **Does not follow symlinks.** If `users/2024.json` is a symlink to `/etc/passwd`, `safeJoin` returns the symlink path; reading it will still escape. Use `fs.realpath()` after `safeJoin` if you need symlink-aware safety.
* **Does not check existence.** Pure path math.
* **Does not normalize Unicode.** Use `path.normalize()` upstream if you need NFC normalization.

### `PathSecurityError`

```typescript theme={null}
class PathSecurityError extends Error {
  name = "PathSecurityError";
}
```

Pattern-match on `err instanceof PathSecurityError` for fine-grained error handling.

## Where it's used by default

The `FileSystemToolkit` is now hardened automatically:

```typescript theme={null}
import { FileSystemToolkit } from "@agentium/core";

const fs = new FileSystemToolkit({
  basePath: "./agent-workspace", // any LLM-supplied path is safeJoin'd into this
  allowWrite: true,
});

const agent = new Agent({ tools: fs.getTools(), ... });
```

Any tool call with a traversal-y path (`../../etc/passwd`) raises `PathSecurityError`, which the tool executor catches and surfaces to the model as a clean error string. The model can self-correct.

For tools you write yourself, route every LLM-supplied path through `safeJoin`:

```typescript theme={null}
import { safeJoin } from "@agentium/core";

const readNote = defineTool({
  name: "read_note",
  parameters: z.object({ path: z.string() }),
  execute: async ({ path }, ctx) => {
    const safe = safeJoin("./notes", path);  // ← hard fence
    return await fs.readFile(safe, "utf8");
  },
});
```

## SSRF protection — `allowedHosts`

URL-fetching toolkits accept an `allowedHosts` option that whitelists exactly which hosts the toolkit is permitted to reach.

### Built-in coverage

`ScraperToolkit` is the canonical example; it accepts an LLM-supplied URL:

```typescript theme={null}
import { ScraperToolkit } from "@agentium/core";

const scraper = new ScraperToolkit({
  allowedHosts: ["wikipedia.org", "nodejs.org", "github.com"],
});

const agent = new Agent({ tools: scraper.getTools(), ... });
```

The model can now only successfully fetch from those three domains. Anything else throws `PathSecurityError("Host blocked by allowedHosts policy: ...")` before any network round-trip.

### Matching rules

| Allowlist entry     | Matches                                                                         |
| ------------------- | ------------------------------------------------------------------------------- |
| `"example.com"`     | `example.com` (exact) AND `api.example.com`, `cdn.example.com` (any sub-domain) |
| `"sub.example.com"` | only `sub.example.com` and its sub-domains                                      |

Sub-domain matching uses suffix comparison after a `.` boundary, so `"example.com"` does **not** accidentally allow `notexample.com`.

### `isHostAllowed` / `assertHostAllowed`

Use these directly in your own URL-accepting tools:

```typescript theme={null}
import { assertHostAllowed, isHostAllowed } from "@agentium/core";

const allowList = ["api.example.com"];

const callApi = defineTool({
  name: "call_api",
  parameters: z.object({ url: z.string() }),
  execute: async ({ url }) => {
    assertHostAllowed(url, allowList); // throws PathSecurityError if denied
    return fetch(url).then((r) => r.text());
  },
});

// Or do a boolean check for soft enforcement:
if (isHostAllowed(url, allowList)) await fetch(url);
```

`isHostAllowed(url, undefined)` and `isHostAllowed(url, [])` always return `true` (no restriction). Pass an explicit array to enforce.

## Defense-in-depth recommendations

Even with `safeJoin` and `allowedHosts`, multiple layers help:

1. **Run the agent as an unprivileged OS user** — even if pathing escapes, the user can't read sensitive files.
2. **Restrict outbound network at the firewall** — `allowedHosts` is a soft fence; iptables / VPC egress rules are the hard fence.
3. **Audit `tool.result` events** — the EventBus emits every tool call; alert on suspicious patterns (many denied requests in a short window).
4. **Use the [`SandboxAgent`](/features/sandbox-agent)** for any agent that runs arbitrary code or shell.

## See also

* [`SandboxAgent`](/features/sandbox-agent) — full filesystem + shell isolation
* [Sandbox Toolkits](/features/sandbox-toolkits) — push code execution into E2B / Daytona
* [Approval Gates](/features/approval-gates) — human-in-the-loop for dangerous tools
