Skip to content

Security Architecture

Seed implements defense-in-depth security using multiple layers of protection to guard against common web vulnerabilities and attacks.

Security Layers Overview

HTTP Security Headers

Seed uses Helmet to set security headers that protect against common web vulnerabilities.

Configuration

File: src/config/helmet.ts

Two configurations are provided:

  1. helmetConfig - Full security headers for HTML responses (documentation)
  2. helmetApiConfig - Relaxed configuration for API routes (no CSP)

Content Security Policy (CSP)

Purpose: Prevents XSS attacks by restricting content sources

Configuration (HTML responses only):

typescript
contentSecurityPolicy: {
  directives: {
    defaultSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"], // VitePress requires inline styles
    scriptSrc: ["'self'"],
    imgSrc: ["'self'", "data:", "https:"],
    connectSrc: ["'self'"],
    fontSrc: ["'self'"],
    objectSrc: ["'none'"],
    mediaSrc: ["'self'"],
    frameSrc: ["'none'"],
    baseUri: ["'self'"],
    formAction: ["'self'"],
    frameAncestors: ["'none'"], // Prevent clickjacking
  },
}

Protection Against:

  • XSS via inline scripts (only allow 'self')
  • Data exfiltration via unauthorized connections
  • Clickjacking via frame embedding
  • Content injection attacks

Note: CSP is disabled for API routes (/mcp, /oauth/*) since they return JSON, not HTML.

Strict Transport Security (HSTS)

Purpose: Enforces HTTPS connections and prevents protocol downgrade attacks

Configuration (production only):

typescript
strictTransportSecurity: process.env.NODE_ENV === "production"
  ? {
      maxAge: 31536000,        // 1 year
      includeSubDomains: true,
      preload: true,
    }
  : false

Why Production Only?

  • Requires valid HTTPS certificate
  • Development environments often use HTTP
  • Can cause issues with self-signed certificates

Protection Against:

  • Man-in-the-Middle attacks
  • Protocol downgrade attacks
  • SSL stripping

Frame Protection

Purpose: Prevents clickjacking attacks

Headers:

typescript
frameguard: {
  action: "deny",  // X-Frame-Options: DENY
}

// Also enforced via CSP
frameAncestors: ["'none'"]

Protection Against:

  • Clickjacking via iframe embedding
  • UI redressing attacks

MIME Sniffing Protection

Purpose: Prevents browsers from MIME-sniffing responses

Configuration:

typescript
noSniff: true  // X-Content-Type-Options: nosniff

Protection Against:

  • MIME confusion attacks
  • Content-Type spoofing

Referrer Policy

Purpose: Controls information sent in Referer header

Configuration:

typescript
referrerPolicy: {
  policy: "strict-origin-when-cross-origin",
}

Behavior:

  • Same-origin: Send full URL
  • Cross-origin (HTTPS → HTTPS): Send origin only
  • Cross-origin (HTTPS → HTTP): Send nothing

Protection Against:

  • Information leakage via Referer header
  • Privacy violations

Additional Headers

DNS Prefetch Control:

typescript
dnsPrefetchControl: {
  allow: false,  // X-DNS-Prefetch-Control: off
}

Cross-Origin Policies:

typescript
crossOriginOpenerPolicy: {
  policy: "same-origin",  // COOP: same-origin
}

crossOriginResourcePolicy: {
  policy: "cross-origin",  // CORP: cross-origin (allows CORS)
}

crossOriginEmbedderPolicy: false  // Disabled to allow cross-origin resources

Origin Agent Cluster:

typescript
originAgentCluster: true  // Origin-Agent-Cluster: ?1

Hide Server Info:

typescript
hidePoweredBy: true  // Remove X-Powered-By header

Permissions-Policy Header

Purpose: Restricts browser features to prevent unauthorized access to sensitive APIs

Implementation: Custom middleware at src/middleware/permissions-policy.ts

Since Seed is an MCP server/OAuth proxy, it doesn't need access to any browser features like camera, microphone, geolocation, etc. The Permissions-Policy header (formerly Feature-Policy) disables all unnecessary browser APIs as a defense-in-depth measure.

Configuration:

typescript
// All features disabled with () = empty allowlist
const policies = [
  "camera=()",                  // No camera access
  "microphone=()",              // No microphone access
  "geolocation=()",             // No location access
  "payment=()",                 // No payment APIs
  "usb=()",                     // No USB device access
  "magnetometer=()",            // No magnetometer access
  "gyroscope=()",               // No gyroscope access
  "accelerometer=()",           // No accelerometer access
  "ambient-light-sensor=()",    // No ambient light sensor
  "autoplay=()",                // No autoplay
  "encrypted-media=()",         // No encrypted media (DRM)
  "fullscreen=()",              // No fullscreen API
  "midi=()",                    // No MIDI device access
  "picture-in-picture=()",      // No picture-in-picture
  "speaker-selection=()",       // No speaker selection
  "sync-xhr=()",                // No synchronous XHR
  "vr=()",                      // No VR APIs (deprecated)
  "xr-spatial-tracking=()",     // No XR spatial tracking
  "display-capture=()",         // No screen capture
];

Applied to: All routes via middleware in src/app.ts

Protection Against:

  • Unauthorized access to device hardware (camera, microphone, USB)
  • Location tracking via geolocation API
  • Side-channel attacks via sensors (gyroscope, accelerometer, magnetometer)
  • Unauthorized screen capture or recording
  • Payment API abuse

Format: feature=(allowlist) where:

  • () = empty allowlist (feature disabled for all origins)
  • 'self' = feature allowed for same origin only
  • * = feature allowed for all origins (not used in Seed)

References:

CORS Policy

Cross-Origin Resource Sharing (CORS) controls which origins can access the API.

Default Allowed Origins

File: src/config/cors.ts

typescript
const defaultOrigins = [
  "http://localhost",
  "http://localhost:3000",
  "http://localhost:5173",
  "https://claude.ai",
  "https://claude.anthropic.com",
];

Environment-Based Additions:

bash
CORS_EXTRA_ORIGINS=https://example.com,https://app.example.com

Configuration

typescript
export const corsConfig = {
  origin: [...defaultOrigins, ...extraOrigins],
  credentials: true,
  methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
  allowedHeaders: [
    "Content-Type",
    "Authorization",
    "mcp-session-id",
  ],
  exposedHeaders: [
    "X-RateLimit-Limit",
    "X-RateLimit-Remaining",
    "X-RateLimit-Reset",
  ],
  maxAge: 86400, // 24 hours
};

Key Features:

  • Credentials Support: Allows cookies and Authorization headers
  • Session Headers: Exposes mcp-session-id for MCP session tracking
  • Rate Limit Headers: Exposes rate limit info to clients
  • Preflight Caching: 24-hour cache for OPTIONS requests

Protection Against

  • Unauthorized cross-origin access
  • CSRF attacks (with proper origin validation)
  • Data exfiltration from untrusted origins

Origin Validation

Origin validation provides an additional layer of protection against DNS rebinding attacks.

DNS Rebinding Attack Overview

Attack Scenario:

  1. Attacker controls domain evil.com
  2. User visits evil.com which loads malicious JavaScript
  3. DNS for evil.com resolves to 127.0.0.1 (localhost)
  4. JavaScript makes requests to http://localhost:3000/mcp
  5. Requests bypass CORS because they appear to come from localhost

How Origin Validation Prevents This:

  • Validates Origin header against whitelist
  • Rejects requests from non-whitelisted origins
  • Works in conjunction with CORS

Implementation

File: src/middleware/origin-validation.ts

Middleware Function:

typescript
export function validateOrigin(req: Request, res: Response, next: NextFunction): void {
  // Skip if origin validation is disabled
  if (!config.security.originValidation.enabled) {
    return next();
  }

  const origin = req.headers.origin;

  // Allow requests without Origin header (CLI tools, native apps)
  if (!origin) {
    return next();
  }

  // Check against allowed origins
  if (isAllowedOrigin(origin)) {
    return next();
  }

  // Log potential attack
  logSecurityEvent("origin_blocked", "medium", {
    origin,
    path: req.path,
    ip: req.ip,
  });

  res.status(403).json({
    jsonrpc: "2.0",
    error: {
      code: -32000,
      message: "Forbidden",
      data: {
        reason: "invalid_origin",
        details: "Origin not allowed by server policy",
      },
    },
    id: null,
  });
}

Pattern Matching

The origin validation supports wildcard patterns for flexible configuration:

Exact Match:

typescript
pattern: "https://claude.ai"
// Matches: https://claude.ai
// Rejects: https://api.claude.ai

Wildcard Subdomain:

typescript
pattern: "*.anthropic.com"
// Matches: https://api.anthropic.com
// Matches: https://app.anthropic.com
// Matches: https://anthropic.com
// Rejects: https://fake-anthropic.com

Wildcard Port:

typescript
pattern: "http://localhost:*"
// Matches: http://localhost:3000
// Matches: http://localhost:5173
// Rejects: http://localhost (no port)

Implementation:

typescript
export function matchOriginPattern(origin: string, pattern: string): boolean {
  // Exact match
  if (origin === pattern) {
    return true;
  }

  // Wildcard subdomain match (e.g., "*.anthropic.com")
  if (pattern.startsWith("*.")) {
    const domain = pattern.slice(2);
    const url = new URL(origin);
    return url.hostname === domain || url.hostname.endsWith("." + domain);
  }

  // Wildcard port match (e.g., "http://localhost:*")
  if (pattern.endsWith(":*")) {
    const basePattern = pattern.slice(0, -2);
    const url = new URL(origin);
    if (!url.port) return false;
    const baseUrl = `${url.protocol}//${url.hostname}`;
    return baseUrl === basePattern;
  }

  return false;
}

Configuration

File: src/config/security.ts

typescript
export const securityConfig = {
  originValidation: {
    enabled: process.env.ORIGIN_VALIDATION_ENABLED !== "false",
  },
  allowedOrigins: [
    "http://localhost",
    "http://localhost:*",
    "https://claude.ai",
    "*.anthropic.com",
    ...(process.env.ALLOWED_ORIGINS?.split(",").map((o) => o.trim()) ?? []),
  ],
};

Environment Variables:

bash
ORIGIN_VALIDATION_ENABLED=true
ALLOWED_ORIGINS=https://example.com,https://app.example.com

Behavior

Requests WITHOUT Origin Header:

  • Allowed through (CLI tools, native apps, Postman)
  • Browser requests always include Origin header

Requests WITH Origin Header:

  • Must match allowed origins list
  • Pattern matching applied (wildcards supported)
  • Blocked requests return 403 Forbidden

Observability

Security Event Logging:

typescript
logSecurityEvent("origin_blocked", "medium", {
  origin: "https://evil.com",
  path: "/mcp",
  ip: "192.168.1.100",
});

Log Format:

json
{
  "timestamp": "2026-01-06T12:00:00Z",
  "level": "warn",
  "event": "origin_blocked",
  "severity": "medium",
  "origin": "https://evil.com",
  "path": "/mcp",
  "ip": "192.168.1.100",
  "service": "seed"
}

JWT Authentication

JWT validation ensures only authenticated users with valid tokens can access protected endpoints.

Key Security Features

Signature Verification:

  • Uses JWKS (JSON Web Key Set) from OIDC provider
  • Supports RSA and ECDSA signature algorithms
  • Automatic key rotation via cache refresh

Claims Validation:

  • Issuer (iss): Must match OIDC_ISSUER
  • Audience (aud): Must match OIDC_AUDIENCE
  • Expiration (exp): Token must not be expired
  • Not Before (nbf): Token must be valid at current time (if present)
  • Subject (sub): User ID must be present

Implementation: See Authentication Flow for detailed documentation.

Protection Against

  • Unauthorized access (missing/invalid tokens)
  • Token forgery (signature verification)
  • Token replay attacks (expiration validation)
  • Cross-tenant attacks (issuer/audience validation)

OAuth Security

The OAuth 2.1 implementation includes several security features per RFC standards.

PKCE (Proof Key for Code Exchange)

Purpose: Prevents authorization code interception attacks

How It Works:

  1. Client generates code_verifier (random string)
  2. Client creates code_challenge (SHA256 hash of verifier)
  3. Authorization request includes code_challenge
  4. Token request includes original code_verifier
  5. Server verifies that SHA256(code_verifier) matches stored challenge

Validation:

typescript
// In token endpoint
const storedCodeChallenge = await getStoredChallenge(code);
const computedChallenge = base64UrlEncode(sha256(code_verifier));

if (computedChallenge !== storedCodeChallenge) {
  throw new Error("Invalid code_verifier");
}

Protection Against:

  • Authorization code interception
  • Public client attacks

Redirect URI Validation

Purpose: Prevents open redirect vulnerabilities

Validation Rules (RFC 7591):

typescript
function validateRedirectUri(uri: string): string | null {
  const url = new URL(uri);

  // Must use HTTPS except localhost
  const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
  if (url.protocol !== "https:" && !isLocalhost) {
    return "redirect_uri must use https";
  }

  // Must not contain fragment (per RFC 6749)
  if (url.hash) {
    return "redirect_uri must not contain a fragment";
  }

  return null; // Valid
}

Enforcement:

  • During Dynamic Client Registration (DCR)
  • During authorization flow
  • Exact match required (no partial matching)

Configuration Limits:

typescript
// Maximum redirect URIs per client
maxRedirectUris: 10

Protection Against:

  • Open redirect attacks
  • Phishing via redirect manipulation
  • Token theft via malicious redirects

Client Storage Security

Redis-Backed Storage:

  • Clients stored with automatic expiration (default: 30 days)
  • No sensitive credentials stored (public clients only)
  • Client IDs prefixed for namespacing

Client ID Generation:

typescript
function generateClientId(): string {
  const prefix = "seed-";
  // 12-character alphanumeric random string
  const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
  const randomPart = Array.from({ length: 12 }, () =>
    chars[Math.floor(Math.random() * chars.length)]
  ).join("");
  return prefix + randomPart;
}

TTL Management:

  • Automatic expiration prevents stale clients
  • Configurable via DCR_CLIENT_TTL (default: 2592000 seconds = 30 days)

Implementation: See OAuth 2.1 Implementation for detailed documentation.

Rate Limiting

Rate limiting protects against abuse and ensures fair resource usage.

Key Security Features

Distributed Limits:

  • Redis-backed sliding window algorithm
  • Works across multiple server instances
  • Per-IP and global limits

Endpoint-Specific Limits:

  • MCP Endpoints: 100 requests/minute per IP
  • DCR Endpoint: 10 registrations/hour per IP

Graceful Degradation:

  • Falls back to allowing requests if Redis unavailable
  • Prevents rate limiting from causing downtime

Implementation: See Rate Limiting for detailed documentation.

Protection Against

  • Brute force attacks
  • Distributed denial of service (DDoS)
  • Resource exhaustion
  • API abuse

Security Best Practices

Environment-Based Configuration

Development:

  • HSTS disabled (HTTP allowed)
  • More verbose logging
  • Less strict rate limits

Production:

  • HSTS enabled (HTTPS enforced)
  • Structured JSON logging
  • Strict rate limits

Configuration Pattern:

typescript
strictTransportSecurity: process.env.NODE_ENV === "production"
  ? { maxAge: 31536000, includeSubDomains: true, preload: true }
  : false

Defense in Depth

Multiple security layers provide redundancy:

  • If origin validation fails, CORS still protects
  • If CORS is bypassed, JWT auth still protects
  • If auth is somehow bypassed, rate limiting still protects

Fail-Secure Defaults

Examples:

  • Rate limiting enabled by default
  • Origin validation enabled by default
  • Authentication required by default
  • Helmet headers applied by default

Disabling Security (not recommended for production):

bash
RATE_LIMIT_ENABLED=false
ORIGIN_VALIDATION_ENABLED=false
AUTH_REQUIRED=false

Observability

All security events are logged with appropriate severity:

  • Info: Successful authentication
  • Warn: Rate limit exceeded, origin blocked
  • Error: JWT validation failures, Redis errors

Structured Logging enables security monitoring and incident response:

json
{
  "timestamp": "2026-01-06T12:00:00Z",
  "level": "warn",
  "event": "origin_blocked",
  "severity": "medium",
  "origin": "https://evil.com",
  "path": "/mcp",
  "ip": "192.168.1.100"
}

Configuration Reference

Environment Variables

bash
# Helmet Security Headers
NODE_ENV=production              # Enable HSTS in production

# CORS
CORS_EXTRA_ORIGINS=https://example.com,https://app.example.com

# Origin Validation
ORIGIN_VALIDATION_ENABLED=true
ALLOWED_ORIGINS=https://example.com,https://app.example.com

# JWT Authentication
AUTH_REQUIRED=true               # Require authentication (default: true)
OIDC_ISSUER=https://auth.example.com/application/o/my-app/
OIDC_AUDIENCE=my-client-id
OIDC_JWKS_URL=https://auth.example.com/application/o/my-app/jwks/

# Rate Limiting
RATE_LIMIT_ENABLED=true          # Enable rate limiting (default: true)
MCP_RATE_LIMIT_WINDOW_MS=60000   # 1 minute
MCP_RATE_LIMIT_MAX=100           # 100 requests per IP per minute
DCR_RATE_LIMIT_WINDOW_MS=3600000 # 1 hour
DCR_RATE_LIMIT_MAX=10            # 10 registrations per IP per hour

# OAuth Security
DCR_CLIENT_TTL=2592000           # 30 days

Implementation Files

  • Helmet Config: src/config/helmet.ts - Security header configuration
  • Security Config: src/config/security.ts - Origin validation configuration
  • CORS Config: src/config/cors.ts - CORS policy configuration
  • Origin Validation: src/middleware/origin-validation.ts - DNS rebinding protection
  • Auth Middleware: src/middleware/auth.ts - JWT validation
  • Rate Limiter: src/middleware/distributed-rate-limit.ts - Rate limiting implementation
  • OAuth DCR: src/routes/oauth-register.ts - Redirect URI validation

Released under the MIT License.