Skip to main content

Overview

All API inputs are validated using Zod schemas before processing. Error messages are sanitized before reaching the client to prevent information leakage. URLs supplied by users or services are validated against SSRF blocklists with DNS rebinding protection. Together, these layers ensure that malformed, malicious, or probing requests never reach business logic.

Zod schema validation

Every API endpoint validates its request body through the validateBody() helper:
const result = await validateBody(request, schema)
if (result.error) return result.error // 400 with issues
const { data } = result // typed, validated data
1

Parse JSON

Attempts request.json(). If the body is not valid JSON, returns a 400 response with "error": "Invalid JSON body".
2

Validate against schema

Runs schema.safeParse(raw). If validation fails, returns a 400 response with an issues array containing the path and message for each violation.
3

Return typed data

On success, returns the parsed and typed data object. Downstream code uses the validated type directly.

Validation error format

{
  "error": "Validation failed",
  "issues": [
    { "path": "email", "message": "Invalid email format" },
    { "path": "password", "message": "Password must be at least 8 characters" }
  ]
}

Schema constraints

Selected schemas and their key constraints:
SchemaNotable constraints
createPasswordSchemaV3name max 200, url max 2000, ciphertext max 1 MB, requires wrapped_item_key, encrypted_data with v: 3
createShareSchemaaccountId must be UUID, permission is read or write, expiresAt is ISO datetime
notificationCreateSchematitle max 500, body max 5000, action_url must be valid URL, metadata is a record
serviceRegistryOverrideSchemadomain validated as hostname, category is enum, selectors checked for script injection
deviceRegistrationSchemafingerprint required, deviceName max 100, deviceType max 50
bulkCreatePasswordsSchemaArray of 1-200 entries, each validated individually
The .strict() modifier is applied to schemas where extra fields should be rejected (e.g., password updates, passkey operations). This prevents unintended field injection.

URL validation

The URL validator prevents server-side request forgery (SSRF) by blocking requests to internal infrastructure, cloud metadata endpoints, and private IP ranges.

Basic validation

The synchronous validateNavigationUrl() runs these checks:
1

Length check

URLs longer than 2048 characters are rejected to prevent resource exhaustion.
2

Protocol check

Only http: and https: protocols are allowed. All others (file:, ftp:, data:, etc.) are blocked.
3

Host blocklist

The hostname is checked against a list of known internal endpoints.
4

IP range check

The hostname is checked against blocked private and reserved IP ranges.

Blocked hosts

HostReason
169.254.169.254AWS EC2 instance metadata
100.100.100.200Alibaba Cloud metadata
metadata.google.internalGCP metadata
127.0.0.1 / localhostLoopback
0.0.0.0Unspecified
::1 / [::1]IPv6 loopback

Blocked IP ranges

CIDRDescription
10.0.0.0/8Private network (Class A)
172.16.0.0/12Private network (Class B)
192.168.0.0/16Private network (Class C)
169.254.0.0/16Link-local
127.0.0.0/8Loopback
0.0.0.0/8Unspecified
fc00::/7IPv6 unique local address
fe80::/10IPv6 link-local

DNS rebinding protection

The async validateNavigationUrlWithDns() extends basic validation with DNS resolution to catch hostnames that resolve to internal IPs:
1

Run basic checks

Executes all synchronous validation first. If the basic check fails, returns immediately.
2

Resolve A records

Calls dns.resolve4(hostname) to get IPv4 addresses. If DNS resolution fails, the URL is rejected (fail-closed).
3

Resolve AAAA records

Calls dns.resolve6(hostname) to get IPv6 addresses. Missing AAAA records are acceptable.
4

Validate resolved IPs

Every resolved IP is checked against the blocked hosts and IP ranges. If any resolved address is internal, the URL is rejected.
DNS rebinding attacks use a hostname that initially resolves to a public IP (passing validation) but later resolves to an internal IP. The DNS validation layer resolves the hostname at request time to catch this pattern. DNS resolution failures are treated as blocked to prevent evasion.

CSS selector validation

Service registry selectors are validated to block script injection:
const safeSelectorSchema = z.string().max(500).refine(
  (s) => !/<script/i.test(s) && !/javascript:/i.test(s) && !/on\w+\s*=/i.test(s),
  "Selector contains unsafe content"
)
Regex pattern strings are validated to ensure they compile:
const safeRegexPatternSchema = z.string().max(500).refine((s) => {
  try { new RegExp(s); return true } catch { return false }
}, "Must be a valid regular expression")

Error sanitization

Error messages returned to the client pass through a sanitization layer that blocks internal details.

Internal patterns blocked

The sanitizer matches against 48 patterns covering:
Database errors
  • Duplicate key / constraint violations
  • Missing tables, columns, relations
  • Permission denied
  • Syntax errors
  • Deadlocks, timeouts
Network / infra errors
  • ECONNREFUSED, ENOTFOUND, ETIMEDOUT
  • Socket hang up, connection reset
  • DNS resolution failures (getaddrinfo)
  • SSL / certificate errors
Auth / system errors
  • JWT errors
  • Row-level security
  • Role / database missing
  • Out of memory
  • Stack depth limit
  • Too many connections

Safe messages

These messages pass through the sanitizer without modification:
  • Not found
  • Unauthorized
  • Forbidden
  • Bad request
  • Method not allowed
  • Conflict
  • Too many requests
  • Service unavailable

Sanitization rules

ConditionResult
Message matches an internal patternReplaced with generic fallback (“An error occurred”)
Message exceeds 200 charactersTreated as a stack trace, replaced with fallback
Message is a known safe messagePassed through unchanged
Message is short and contains no internal patternsPassed through unchanged

Database error sanitization

The sanitizeDbError() function always returns a generic fallback message regardless of the input. Database errors never reach the client.
function sanitizeDbError(_error: unknown, fallback = "A database error occurred"): string {
  return fallback
}
The unconditional fallback for database errors is intentional. Even seemingly benign Supabase/PostgreSQL error messages can reveal table names, constraint names, or query structure that aid attackers in fingerprinting the stack.