Skip to main content

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

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)

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

AttackExample inputBehavior
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 escapesafeJoin("/etc", "passwd") from base /etcfoodistinct 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

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:
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:
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:
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 entryMatches
"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:
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 firewallallowedHosts 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 for any agent that runs arbitrary code or shell.

See also