Security model

The defaults are on. This page lists what they protect against, how, and where the protection ends.

Where this fits: Run and operate. What Kervan protects against, for every tool and for spec tools.

On this page

Defaults for every server

  • Timeouts: 30 seconds per tool call by default (limits.toolTimeoutMs); cancellation and timeouts reach the handler as ctx.signal.
  • Size limits: request bodies (4 MiB over HTTP), argument size (10,000 array elements and object members), and for spec tools response sizes, output length and spec size.
  • Error masking: only ToolError messages reach the client. Anything else becomes Internal error in tool "x" (ref: ...), with the real error in your logs under that ref.
  • HTTP: binds to 127.0.0.1 by default; Host and Origin are checked against allowedHosts and allowedOrigins (DNS rebinding protection); a per-client rate limit (300 requests a minute) on Node; logs go to stderr, never stdout.
  • Validation: arguments are checked against the input schema before a handler runs, and structured results against the output schema.

Network (SSRF) protection

Spec tools make HTTP requests to URLs a spec author writes and a model fills in. These checks keep them on the public internet:

Every request, and every redirect hop, goes through the same checks, and each fails closed:

  1. The URL is parsed with the WHATWG parser, which turns encodings such as 2130706433, 0x7f.1 or 0177.0.0.1 into 127.0.0.1; the scheme, host and port are literal in the spec.
  2. The host name is resolved once, within the request timeout. Empty answers, resolver errors and anything that is not a strictly valid IP address (including IPv6 zone IDs such as %eth0) block the request.
  3. Every address must be public unicast: the address classifier (ipaddr.js) must say unicast and the address must be outside an independent list of internal ranges (loopback, private, link-local and cloud metadata such as 169.254.169.254, CGNAT, unique local, multicast, documentation, benchmarking, NAT64, 6to4, Teredo, every IPv4-mapped IPv6 address, and all IPv6 space outside the global unicast block 2000::/3). One internal address among public ones blocks the whole request.
  4. The connection is pinned to the checked addresses through a custom lookup (no second DNS query, so no DNS rebinding window), on a fresh connection, and the socket’s remote address is checked again when it connects.
  5. Redirects are not followed unless followRedirects allows it. Each hop is resolved and checked again; https to http downgrades, credentials in the location and other schemes are refused; when the origin changes, every header from the spec is dropped except Accept and User-Agent (templated or literal, any of them may be a credential), and a request body is never sent to another origin.
  6. This machine’s own addresses (every address of os.networkInterfaces(), public ones included) are refused, because services listening on all interfaces are reachable through them. If the list cannot be read, requests are refused.
  7. At most a few DNS lookups run at once, process-wide (half of libuv’s thread pool, see below); others queue and give up when the request times out.

Blocked requests report the reason but never the resolved address, which could reveal internal DNS names.

The order of the rules is: cloud metadata (always refused, see below), network.denyList (always refused), network.allowPrivate (explicitly allowed), this machine’s addresses, then the public-unicast checks.

Cloud metadata endpoints are refused before any other rule, so neither allowPrivate nor --allow-private-network reaches them (METADATA_RANGES):

  • all of link-local 169.254.0.0/16 (the metadata service of AWS, GCP, Azure, Oracle, DigitalOcean, OpenStack; ECS/EKS credential agents; Tencent);
  • fd00:ec2::/32 (AWS over IPv6);
  • 100.100.100.200 (Alibaba);
  • 168.63.129.16 (Azure WireServer, a public address);
  • 192.0.0.192 (Oracle, legacy). For local development, kervan run --allow-private-network (refused with NODE_ENV=production) or network.allowPrivate in code allows internal addresses; --deny-network <cidr> or network.denyList blocks more. A spec file can set neither.

DNS lookups and UV_THREADPOOL_SIZE

dns.lookup runs on libuv’s thread pool, which also serves file system and crypto work, and a lookup cannot be cancelled: a name server that never answers keeps its thread busy until the operating system gives up. Kervan therefore lets at most half of the pool (2 of the default 4 threads) resolve names at once; a lookup’s slot is freed only when the lookup itself ends, and queued requests fail with “timed out waiting for a DNS lookup slot”. To allow more concurrent lookups, start Node.js with a larger pool, e.g. UV_THREADPOOL_SIZE=16 (8 lookups).

Secrets and untrusted output

  • Secret values are redacted from results, errors and logs, in their raw, URL-encoded, form-encoded and JSON-escaped forms, and from the response before select runs. Encodings Kervan does not know (base64, HTML entities) are not redacted: bind secrets only to APIs you trust not to echo them. See secrets.
  • An API response is untrusted input for the model. select limits it to chosen fields, but text inside a chosen field can still carry instructions (prompt injection); Kervan cannot judge content.

How the checks are tested

Each address check, redaction form and limit has tests that show it working. They were also mutation-tested: each check was broken on purpose, one at a time, and the run counted only when a test failed. That was done by hand during development, not on every change; the open questions the security reviews left are listed in the repository’s docs/REVIEW-NOTES.md.

Known limits

  • Node.js only: the executor uses node:http and node:dns. Fetch runtimes (Workers, Deno) cannot pin DNS, so specs are not supported there.
  • No proxy support: HTTPS_PROXY/HTTP_PROXY are ignored. Behind a mandatory outbound proxy, spec tools cannot reach the internet.
  • Regular expressions in schemas (pattern) run on the JavaScript engine; the length limit reduces, but does not remove, the risk of slow patterns (ReDoS) from spec authors.
  • Rate limits are per process.

Reporting a vulnerability

Report it privately, through the repository’s private vulnerability reporting or to [email protected], never in a public issue. Reports are acknowledged within 5 working days, and disclosure is coordinated (90 days). The security.txt of this site has the contact too.