Skip to main content

API Security

Rate limiting

Per API key with project-level defaults:

  • Delivery: 100 req/s
  • Management: 30 req/s

Per-key overrides in the admin UI's API key form. Counts are tracked with an in-process sliding window per server instance (in-memory, no database round-trip). Response headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1714592400

429 on overage with a Retry-After header. The SDK's retry loop honors both.

Coarse per-IP rate limit also runs in the Edge middleware (60 req/s default) so anonymous hammering doesn't exhaust capacity.

Scalability

The per-key counter lives in process memory on each server instance, so counts are not shared across instances or regions — a key spread over N serverless instances can effectively burst to N× the configured limit. The limit is best-effort, not a hard global ceiling. High-traffic delivery should lean on edge/CDN caching (delivery responses are edge-cacheable — see Caching) or move the counter to a shared store (Redis) if you need a strict cross-instance limit.

CORS

Per-project for management routes (allow-list configured under Project.settings.cors). Per-site for delivery (the site's hostnames double as the CORS allow-list).

The middleware emits the right headers on preflight (OPTIONS) requests; a disallowed origin gets no Access-Control-Allow-Origin header at all (it's omitted, not empty), so the browser blocks.

Security headers

The following response headers are set globally:

X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: ...
X-XSS-Protection: 1; mode=block
CSP — planned

A strict Content-Security-Policy for the admin UI is planned but not yet set. Delivery + GraphQL responses don't set CSP either — that's the host frontend's responsibility.

CSRF

Cookie-session mutations are protected by SameSite=Lax session cookies plus a same-origin Origin/Referer check on state-changing requests. There is no separate CSRF token header.

Bearer-token API requests are exempt — they're not vulnerable to CSRF (the browser cannot attach a Bearer token cross-origin without an explicit fetch from the attacker's origin, and CORS will reject that).

Input validation

  • All API routes parse body via Zod. 422 with details on shape mismatch.
  • Rich text validated against the field's richTextConfig at save.
  • HTML sanitized on the server side of richtext rendering — never trust client HTML.
  • Upload validation: MIME magic-byte check, size cap, extension allow-list per field config.
  • SQL injection prevented by Prisma parameterized queries; no $queryRaw / $executeRaw in production paths.
  • GraphQL query depth limit 10 + complexity limit 1000; introspection disabled for delivery keys.

IP allowlisting (V3)

Project-level gate on /api/v1/*. Configured under Settings → Security → IP allowlist:

  • Project.settings.ipAllowlist.enabled = true activates enforcement.
  • Localhost (127.0.0.1, ::1) always permitted.
  • Self-lockout guard: enabling refuses if the caller's IP isn't already in the allowlist.
  • Delivery API (/api/delivery/*, /api/graphql/*) is never affected.
  • Optional applyToAdmin extends gate to the admin UI.

The CIDR check uses ip-range-check; a single ? IP can be allowed or a CIDR block (203.0.113.0/24).

Errors and leakage

Production errors never leak stack traces, schema details, internal paths, or cross-tenant data. Structured error responses only:

{
"error": "version_conflict",
"message": "Version mismatch: client sent 4, current is 5.",
"details": { "currentVersion": 5, "clientVersion": 4 }
}

500-class errors return a generic message and a request ID. Full error stays in the structured logs.

Webhook security

  • Minimum 32-byte secret. Generated by the management API; the customer can also supply their own.
  • Every outbound payload carries X-Krios-Signature: sha256=<hex_hmac_sha256>. The HMAC is computed over the raw request body. The body is a JSON envelope { event, eventId, timestamp, data, project } — so timestamp and eventId are themselves covered by the signature.
  • Receiver computes HMAC-SHA256 over the raw body and compares it to the sha256= value (constant-time), rejecting on mismatch.

The SDK ships an async verifyKriosSignature(headerValue, rawBody, secret, options?) helper (Web Crypto — runs in Node, edge, and the browser):

import { verifyKriosSignature, parseKriosWebhook } from "@krios/sdk";

const raw = await req.text();
// `toleranceSeconds` is opt-in replay protection: reject deliveries whose
// signed `timestamp` is older than the window (5 min here). Omit it to skip.
const ok = await verifyKriosSignature(
req.headers.get("x-krios-signature"),
raw,
process.env.WEBHOOK_SECRET!,
{ toleranceSeconds: 300 },
);
if (!ok) return new Response("invalid signature", { status: 401 });

const event = parseKriosWebhook(raw);
// Krios retries failed deliveries, so dedupe on event.eventId for
// at-most-once processing.

Request correlation

Every request gets a unique X-Request-Id (incoming or server-generated). The ID is propagated through every database query, audit log entry, structured log line, and response header. Use it in error reports and support tickets.