Security

SpaceNode takes security seriously. The framework includes 12 layers of built-in protection that work out of the box — with zero configuration and zero dependencies. Every protection listed below is implemented in the framework core, battle-tested under stress, and incurs no measurable performance overhead.

Philosophy: Security should be on by default. Developers shouldn't need to remember to enable protections — they should need to explicitly opt out if needed. SpaceNode follows this principle for every layer below.

1. Directory Traversal Protection

Attack: An attacker sends GET /../../../etc/passwd or GET /%2e%2e/%2e%2e/etc/passwd to escape the static files directory and read arbitrary server files.

Protection — 3 layers:

  1. URL decodingdecodeURIComponent() is applied first, so %2e%2e tricks are decoded before any checks.
  2. Path normalization — Node.js path.normalize() resolves all .. segments into a canonical path.
  3. Prefix check — The resolved absolute path must start with the configured static directory. If not, the request is rejected.
// Internal (simplified):
const decoded  = decodeURIComponent(urlPath)
const safe     = normalize(decoded)
const filePath = join(staticDir, safe)
if (!filePath.startsWith(staticDir)) return false  // blocked

Result: No matter how creative the traversal attempt (.., %2e%2e, ..%5c, ..\/), the file path cannot escape the configured directory. This protection is always active when static file serving is enabled.

2. Prototype Pollution Prevention

Attack: An attacker sends a request with field names like __proto__[isAdmin]=true or constructor.prototype.isAdmin=true to modify the JavaScript Object prototype and escalate privileges.

Protection: All body parsers (JSON, URL-encoded forms, and multipart form-data) filter out dangerous keys before they reach your handler:

These keys are silently dropped during parsing. Your handlers never see them.

// This malicious POST body:
// { "__proto__": { "isAdmin": true }, "name": "hacker" }
//
// Becomes in your handler:
// body = { name: "hacker" }  ← __proto__ stripped

Result: Prototype pollution is impossible through SpaceNode's body parsers, regardless of content type. This protection is always active.

3. Default Security Headers

SpaceNode automatically sets essential security headers on every response — static files, API routes, error pages, everything:

HeaderValuePurpose
X-Content-Type-OptionsnosniffPrevents MIME-type sniffing attacks. Browser will not try to guess content types.
X-Frame-OptionsDENYPrevents clickjacking by disallowing the page from being embedded in iframes.

These headers have zero performance cost and protect against the most common browser-level attacks.

4. Security Pipe (Extended Headers)

For stricter protection, add the security pipe. It sets a comprehensive set of headers recommended by OWASP:

const app = await createApp({
  pipe: ['security'],          // standard mode
  // or
  pipe: ['security:strict'],   // strict mode (adds CSP, Permissions-Policy)
})
HeaderStandardStrict
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
X-XSS-Protection0
Referrer-Policystrict-origin-when-cross-origin
X-Permitted-Cross-Domain-Policiesnone
Strict-Transport-Securitymax-age=31536000; includeSubDomains
Content-Security-Policy✓ (restrictive)
Permissions-Policy✓ (camera, mic, geo, payment disabled)
Cross-Origin-Opener-Policysame-origin
Cross-Origin-Resource-Policysame-origin

Recommendation: Use 'security' for APIs, 'security:strict' for web apps serving HTML.

5. Cookie Hardening

When you set a cookie with request.cookie(), SpaceNode applies secure defaults automatically:

FlagDefaultPurpose
HttpOnlyonCookie is not accessible via document.cookie — prevents XSS from stealing session tokens.
SameSiteLaxCookie is not sent on cross-site requests — prevents CSRF attacks.
Path/Cookie is available on all paths (explicit is safer than browser guessing).
// These two are equivalent:
request.cookie('session', token)
request.cookie('session', token, { httpOnly: true, sameSite: 'Lax', path: '/' })

// To opt out (e.g. for a client-side readable cookie):
request.cookie('theme', 'dark', { httpOnly: false })

Result: Session cookies are secure by default. Developers must explicitly opt out to create insecure cookies.

6. CORS Protection

The built-in cors guard reflects the incoming Origin header and adds Vary: Origin for proper cache behavior. If no Origin header is present in the request, it falls back to Access-Control-Allow-Origin: *:

// Reflects the requesting origin (safe — only the caller sees the header)
pipe: ['cors']

// Lock to a specific origin (recommended for production)
pipe: ['cors:https://mysite.com']

The CORS guard also sets Access-Control-Allow-Credentials: true (allows cookies and auth headers cross-origin) and Access-Control-Max-Age: 86400 (browsers cache the preflight for 24 hours). On preflight (OPTIONS), the Access-Control-Allow-Headers value is mirrored from the client’s Access-Control-Request-Headers header.

Why this matters: A wildcard Access-Control-Allow-Origin: * allows any website to make requests to your API. For production, lock the origin to a specific domain with cors:https://mysite.com to control exactly who can call your endpoints from a browser.

7. WebSocket Origin Validation

Attack: Cross-Site WebSocket Hijacking (CSWSH). A malicious webpage opens a WebSocket to your server. Since browsers send cookies automatically, the attacker can piggyback on the victim's session.

Protection: Configure allowed origins for WebSocket connections:

const app = await createApp({
  wsOrigins: ['https://mysite.com', 'https://admin.mysite.com'],
})

app.ws('/chat', (socket, req) => {
  // Only connections from allowed origins reach here
})

Any WebSocket upgrade request with a mismatched Origin header receives 403 Forbidden and is immediately destroyed.

wsOrigins valueBehavior
['https://mysite.com']Only this origin allowed
'https://mysite.com'Single string — same as above
['*']All origins allowed (development only)
Not setNo Origin check (backward compatible)

Recommendation: Always set wsOrigins in production when using WebSockets.

8. ReDoS Protection

Attack: Regular Expression Denial of Service. An attacker sends a specially crafted string that makes a regex take exponential time, freezing the Event Loop.

Protection in DTO validation: The pattern validator limits regex patterns to 200 characters and wraps execution in try/catch:

// Safe — pattern is bounded:
export const userDto = {
  username: ['string', 'required', 'pattern:^[a-zA-Z0-9_]{3,20}$'],
}

// A 500-character malicious regex → silently fails validation (returns false)
// An invalid regex → caught, returns false (no crash)

Result: DTO pattern validation cannot be exploited to hang the server, regardless of the input.

9. Information Leak Prevention

SpaceNode prevents information leaks at three levels:

9a. No Path in 404 Responses

When a route is not found, SpaceNode returns:

{ "error": "Not found" }

The requested path is not included in the response. This prevents attackers from enumerating routes or confirming internal paths.

9b. Stack Traces Hidden by Default

On unhandled errors, the server returns:

{ "error": "Internal Server Error" }

Stack traces, file paths, and error messages are only included when debug: true is explicitly set. In production, attackers see nothing useful.

9c. OpenAPI Disabled by Default

The /openapi.json endpoint that exposes your entire API schema (routes, DTOs, types) is disabled by default. It only activates when you explicitly enable it:

const app = await createApp({
  openapi: true,                    // enable with defaults
  // or
  openapi: { title: 'My API' },     // enable with custom config
})

Result: Your API surface is not discoverable unless you intentionally expose it.

10. Body Size Limits

Attack: An attacker sends a massive request body (e.g. 1 GB) to exhaust server memory.

Protection: Request bodies are limited to 1 MB by default. The body is read incrementally — if the limit is exceeded mid-stream, the connection is immediately destroyed and a 413 Payload Too Large error is returned. The full body is never buffered.

// Increase if needed:
const app = await createApp({
  bodyLimit: 10 * 1024 * 1024,  // 10 MB
})

11. Rate Limiting

Attack: Brute-force login attempts, credential stuffing, or simple DDoS from single IPs.

Protection: Built-in per-IP rate limiter, configurable per-route or globally:

// Per-route: 10 login attempts per minute
['POST', '/login', 'login', ['rateLimit:10']]

// Global: 500 requests per minute per IP
const app = await createApp({
  pipe: ['rateLimit:500'],
})

When the limit is exceeded, the client receives 429 Too Many Requests. The rate limit window is 1 minute, and stale entries are automatically cleaned from memory.

12. Request Timeouts & Connection Draining

Attack: Slowloris — an attacker opens many connections and sends data very slowly, tying up server resources.

Protection:

Recommended Production Configuration

This configuration enables all available protections:

import { createApp, defineAuth } from 'spacenode'

defineAuth(async (token) => {
  // Your token verification logic
})

const app = await createApp({
  db: dbConnection,

  // Security
  pipe: [
    'security:strict',              // HSTS, CSP, X-Frame-Options, etc.
    'cors:https://mysite.com',       // locked CORS origin
    'rateLimit:500',                 // 500 req/min per IP
    'compress',                      // response compression
    'logger',                        // request logging
  ],
  wsOrigins: ['https://mysite.com'], // WebSocket origin whitelist
  bodyLimit: 5 * 1024 * 1024,       // 5 MB max body

  // Timeouts
  timeout: 15000,                    // 15s request timeout
  keepAliveTimeout: 5000,
  shutdownTimeout: 10000,

  // Debug off in production (no stack traces)
  debug: false,
})

app.onError((err, req) => {
  // Send to your monitoring service (Sentry, Datadog, etc.)
})

app.listen(process.env.PORT || 3000)

// Graceful shutdown
process.on('SIGINT',  () => app.close(() => process.exit(0)))
process.on('SIGTERM', () => app.close(() => process.exit(0)))

Security Checklist

LayerDefaultAction Required?
Directory Traversal✓ Always activeNone — automatic
Prototype Pollution✓ Always activeNone — automatic
X-Content-Type-Options✓ Always activeNone — automatic
X-Frame-Options✓ Always activeNone — automatic
Cookie HttpOnly + SameSite✓ Always activeNone — automatic
Body Size Limits✓ 1 MB defaultAdjust bodyLimit if needed
ReDoS Protection✓ Always activeNone — automatic
No Info Leaks✓ Always activeKeep debug: false in production
OpenAPI Hidden✓ DisabledEnable with openapi: true only if needed
Request Timeouts✓ 30s defaultTune for your use case
Security Headers (full)Opt-inAdd 'security' or 'security:strict' pipe
CORSOpt-inAdd 'cors:https://yoursite.com' pipe
Rate LimitingOpt-inAdd 'rateLimit:N' pipe
WebSocket OriginOpt-inSet wsOrigins in production

10 of 14 protections are active with zero configuration. The remaining 4 require a single line each to enable.