Skip to content

Security: HumanGuardrail/HuGR_Tools

Security

docs/SECURITY.md

Security Model

Audience: operators deploying @hugr/kernel-based MCP servers. Last updated: 2026-05-28.

This document describes the threat model, security controls, and operational guidelines for the HuGR toolkit MCP server layer. All controls are implemented in packages/kernel/src/mcp/server.ts and packages/kernel/src/mcp/cors.ts.


1. Threat model

Threat Severity Mitigation
Unauthenticated tool calls Critical Bearer token required; IdentityProvider validates every request
Unauthorised cross-origin requests (CSRF / XSS pivot) High CORS allow-list; wildcard "*" forbidden in production
Credential/token leakage in error messages High ToolExecutionError + sanitizeForError; raw errors never forwarded
Brute-force / DoS via tool spam Medium Sliding-window rate limit per user (default: 100 req / 60 s)
Privilege escalation (free user calls premium tool) Medium Per-tool tier check before handler invocation
Invalid or oversized inputs Low–Medium Zod schema validation at every tools/call boundary
Audit gap — no record of what was called Low auditSink hook emits per-call event; no-op default, injectable in prod

Out of scope for this document: network-layer DDoS (handled by Cloudflare), secret rotation (operator-managed), and platform-level identity federation (see docs/architecture/KERNEL_SPEC.md).


2. Input / output safety guarantees

2.1 Input validation

Every tools/call request validates arguments against the tool's Zod schema before the handler is invoked. If validation fails the server returns HTTP 400 / JSON-RPC -32602 with a human-readable details field. Raw Zod errors are formatted and included; no stack traces are forwarded.

Source: packages/kernel/src/mcp/server.ts — Step 8 (Zod validate).

2.2 Output sanitisation

Handler errors are caught and converted through handleError + sanitizeForError (packages/kernel/src/formatter/errors.ts). The resulting ToolError contains only:

  • error — a stable error-code enum value (e.g. "provider_error")
  • message — a safe summary (no paths, tokens, or raw API responses)
  • hint — a user-actionable suggestion

Raw provider error messages, file paths, environment variable values, and API tokens are never forwarded to callers. Handlers must throw ToolExecutionError for structured errors; arbitrary Error throws are caught and downgraded to "internal".

2.3 No any in public exports

All public types exported from @hugr/kernel/mcp are fully typed. Internal as any casts (e.g. for parsed Zod output passed to handlers) are annotated with a comment.


3. Secret handling

Credential model: BYOK (bring-your-own-key). Provider credentials are never stored on a worker. Every worker reads the caller's provider credentials from per-request HTTP headers (e.g. CF-API-TOKEN, DD-API-KEY + DD-APPLICATION-KEY, AWS-ACCESS-KEY-ID + AWS-SECRET-ACCESS-KEY + AWS-REGION) and fails closed ("Missing <provider> credentials") when a required header is absent. This is a multi-tenant requirement: a worker-stored provider key would serve one tenant.s provider data to every user. The per-request header map is enforced in each worker's buildProvider (see packages/kernel/src/identity/hugr-auth.ts).

Secret / credential Location Access pattern
HuGR API key (caller identity) Authorization request header Read once in IdentityProvider.authenticate; never stored
Provider credentials (e.g. CF-API-TOKEN) Caller's per-request HTTP header (BYOK) Lifted into user.extras by hugrAuthIdentity, read by buildProvider per request; never stored on the worker, not logged, not forwarded
Platform auth (HUGR_INLINE_HMAC_SECRET / HUGR_INTROSPECT_URL) Worker env/secret The toolkit's own bearer-verification config — not a provider credential
Dev mode flag HUGR_DEV_MODE env var Bypasses authentication only in development; never set in production Workers

Rule: process.env reads are only permitted inside buildProvider factories and config helpers. Handler modules and tool modules must not read environment variables directly. Provider credentials are never read from env — they arrive per-request as headers.


4. CORS policy

4.1 Configuration

createServer accepts corsOrigins: string[] | "*" (field in ServerConfig, packages/kernel/src/mcp/types.ts).

Value Behaviour When to use
"*" (default) All origins allowed; Access-Control-Allow-Origin: * Development, local tooling, MCP clients without a browser
string[] Only listed origins reflected; others receive 403 Any production deployment exposed to browser clients

Warning: When corsOrigins is "*" and NODE_ENV === "production", the kernel emits a console.warn on server startup. This warning is intentional: operators must acknowledge the risk or supply an allow-list.

Source: packages/kernel/src/mcp/cors.tswarnIfWildcardInProduction.

4.2 Enforcement

  • OPTIONS preflight: corsPreflightResponse returns 204 for allowed origins, 403 for blocked ones.
  • Non-browser POST (no Origin header): always allowed, regardless of corsOrigins. MCP CLI clients and server-to-server calls do not send an Origin header; blocking them would break standard usage.
  • Browser POST from blocked origin: returns 403 JSON-RPC error with cors_forbidden code before authentication runs. This prevents any information leakage to disallowed origins.
  • Vary: Origin: set on all responses when an explicit allow-list is active, ensuring correct CDN caching semantics.

4.3 Production checklist

corsOrigins: [
  "https://your-app.example.com",
  "https://staging.example.com",
]
  • corsOrigins is an explicit list (not "*")
  • The list includes only origins you control
  • NODE_ENV=production is set so the startup warning fires if the list is accidentally omitted
  • HUGR_DEV_MODE is absent or "false" in production

5. Rate limiting

5.1 Default policy

When config.rateLimit is absent, the server applies:

// packages/kernel/src/mcp/types.ts
export const DEFAULT_RATE_LIMIT_POLICY: RateLimitPolicy = {
  windowMs: 60_000,   // 1-minute sliding window
  maxRequests: 100,   // per authenticated user.id
};
  • Scope: per user.id, not per IP. If a caller has multiple IPs, they share one bucket.
  • Algorithm: sliding fixed-window (resets fully at windowStart + windowMs).
  • Storage: in-memory Map in the Worker process. Limits are not shared across Worker instances or restarts.
  • Response: HTTP 429 / JSON-RPC -32000 with error: "rate_limited" and a "Resets in Ns." message.

5.2 Overriding the default

Pass an explicit rateLimit in ServerConfig to loosen or tighten per host:

createServer({
  // ...
  rateLimit: { windowMs: 60_000, maxRequests: 30 }, // stricter: 30 req/min
});

Setting maxRequests very high (e.g. Infinity) effectively disables rate limiting — not recommended for production.

5.3 Interaction with tier checks

Rate limits apply after the tier check. A free user calling a premium tool is rejected at tier-check before consuming a rate-limit slot.


6. Audit trail

6.1 Event shape

After every successful authenticated tools/call dispatch, the server calls config.auditSink with an AuditEvent:

// packages/kernel/src/mcp/types.ts
interface AuditEvent {
  ts: string;         // ISO-8601 timestamp
  userId: string;     // authenticated user.id
  plan: string;       // "free" | "pro" | ...
  toolName: string;   // name of the tool invoked
  status: "ok" | "error"; // "error" = handler threw, result is graceful degradation
  durationMs: number; // wall-clock handler time
}

6.2 Default sink (no-op)

The default auditSink is () => undefined. No audit events are emitted or stored unless a real sink is wired in.

6.3 Wiring a real sink

Inject a sink in ServerConfig when creating the server:

// Example: Cloudflare Analytics Engine
createServer({
  // ...
  auditSink: (event) => {
    analyticsDataset.writeDataPoint({
      blobs: [event.userId, event.toolName, event.status],
      doubles: [event.durationMs],
      indexes: [event.plan],
    });
  },
});

6.4 What is NOT audited

  • Unauthenticated requests (rejected before a user is known)
  • CORS preflight OPTIONS requests
  • initialize and tools/list method calls (informational, no data access)
  • Tier-check rejections (logged separately by ToolExecutionError pattern if desired)

7. Vulnerability disclosure

To report a security issue, email the maintainer at the address in package.json. Do not open a public GitHub issue for vulnerabilities. We target a 7-day initial response for high-severity reports.


File path: docs/SECURITY.md — owned by . Update this document when rate-limit defaults, CORS policy, or audit shape changes.

There aren't any published security advisories