Heximon Logo
Capabilities

Security Hardening

Body-size guard (content-length, 413 RFC-9457), SecurityHeaders (nosniff/DENY/no-referrer/HSTS), RateLimitStore port, MemoryRateLimitStore, RateLimitMiddleware, @heximon/http/security, bearer-only CSRF posture.

A few defaults go a long way: cap the request body so a client cannot exhaust memory, add defensive response headers so browsers enforce content-type sniffing and framing policies, and optionally layer a rate limiter on top. Heximon applies the first two automatically at the dispatch seam — before any handler runs — and ships the rate-limit tier as an opt-in subpath.

Body-size guard

Every incoming request is checked against a maximum body size before any handler or validator reads the body. A request that declares a Content-Length header exceeding the limit is rejected with a 413RFC 9457 problem+json response:

{
  "type": "tag:api.example.com,2026-07-05:BodySizeExceededError",
  "status": 413,
  "title": "Payload Too Large",
  "detail": "Request body size 2097152 bytes exceeds the 1048576 byte limit.",
  "instance": "/upload"
}

(The type tag carries your API's host + the error's class name — the same envelope every framework error renders through.)

The default limit is 1 MiB. Adjust it by setting http.maxBodyBytes in CoreConfig — the host boot prelude wires the value into RequestDispatcher.configure automatically (no app-side call needed):

// In your app config layering — e.g. an AppConfig that maps env into CoreConfig:
const app = await createApp({
  CoreConfig: new CoreConfig({ http: { maxBodyBytes: 5 * 1024 * 1024 } }), // 5 MiB
});

The guard covers both inline and contract-mode controller binding — it sits at the shared dispatch seam, not at the per-route level, so every route is protected automatically.

Security response headers

Every response carries a conservative set of security headers, applied at the same dispatch seam so no route can accidentally omit them:

HeaderValueEffect
X-Content-Type-OptionsnosniffPrevents browsers from MIME-sniffing a response away from the declared content type.
X-Frame-OptionsDENYBlocks the page from being embedded in any <iframe> — prevents clickjacking.
Referrer-Policyno-referrerSends no Referer header on outbound navigations — avoids leaking your URL.
X-DNS-Prefetch-ControloffDisables browser DNS prefetching, which can leak visited URLs to DNS resolvers.
Strict-Transport-Securitymax-age=31536000; includeSubDomainsEnforced only over HTTPS. Tells browsers to always use HTTPS for one year and all subdomains.

HSTS is conditional — it is only emitted when the request arrived over HTTPS, because sending it over HTTP is counterproductive and some environments serve localhost over plain HTTP. The other four headers apply unconditionally.

The header set is applied via SecurityHeadersPolicy, an overridable DI token — bind your own implementation to replace the defaults.

To skip the pass entirely — for an app that sets these headers at the edge/CDN (Vercel/Netlify/Cloudflare header config) rather than per response — turn it off in CoreConfig:

const app = await createApp({
  CoreConfig: new CoreConfig({ http: { securityHeaders: false } }),
});

The overridable SecurityHeadersPolicy still governs which headers apply while on; this toggle is the separate "apply them at all" switch (default on — secure by default). A sibling http toggle turns off other per-request work the same way: requestId: false skips the x-request-id derivation + echo, for apps where correlation ids are assigned upstream (load balancer / API gateway).

Rate limiting — @heximon/http/security

The rate-limit tier is opt-in and lives in the @heximon/http/security subpath. It is designed for per-isolate limiting — correct for a single-process Node app, but not distributed across multiple Cloudflare Workers isolates:

import { RateLimitMiddleware, RateLimitStore, type RateLimitPolicy } from "@heximon/http/security";
import { MemoryRateLimitStore } from "@heximon/http/security";

RateLimitStore — the port

RateLimitStore is the abstract DI token. A concrete implementation owns the window algorithm and its own atomicity. The built-in MemoryRateLimitStore uses an in-memory sliding window — suitable for development and single-process production deployments.

RateLimitMiddleware — a local delegating wrapper

RateLimitMiddleware is not itself discovered as a middleware — it's a plain class you construct directly with a RateLimitStore and a RateLimitPolicy, then delegate to from a local implements Middleware wrapper the HTTP compiler plugin can discover by its own implements Middleware declaration. This is the pattern the flagship app's own rate-limit demo uses:

src/security/security-rate-limit.middleware.ts
import { type HttpAction, type Middleware, type Next } from "@heximon/http";
import { MemoryRateLimitStore, RateLimitMiddleware } from "@heximon/http/security";
import { TimeSpan } from "@heximon/primitives";

/**
 * A per-route rate-limit gate — a discovered `implements Middleware` that delegates to the shipped
 * {@link RateLimitMiddleware} over its own in-memory store, throttling to 5 requests / 30 s per client IP
 * (the shipped default key: `x-forwarded-for`).
 */
export class SecurityRateLimitMiddleware implements Middleware {
  /** The shipped rate-limit middleware, built over a dedicated in-memory store + a fixed policy. */
  private readonly delegate: RateLimitMiddleware = new RateLimitMiddleware(new MemoryRateLimitStore(), {
    limit: 5,
    window: new TimeSpan(30, "s"),
  });

  /**
   * Apply the rate-limit gate, delegating to {@link RateLimitMiddleware} (keyed by client IP).
   *
   * @param action - The matched route's action.
   * @param next - The downstream channel.
   * @returns The downstream response, or a `429` when the per-IP limit is exceeded.
   */
  public handle(action: HttpAction, next: Next): Promise<Response> {
    return this.delegate.handle(action, next);
  }
}

Provide the wrapper and name it on the route that needs it — either per-route via the action's middlewares type argument, or module-wide under http: { middlewares }:

src/security/security.module.ts
import { Module } from "@heximon/runtime";
import { SecurityController } from "./security.controller";
import { SecurityRateLimitMiddleware } from "./security-rate-limit.middleware";

export class SecurityModule extends Module({
  providers: [SecurityRateLimitMiddleware],
  http: { controllers: [SecurityController] },
}) {}
src/security/security.controller.ts
export class SecurityController implements Controller<"/rate"> {
  public async limited(action: Get<"/limited", { middlewares: [SecurityRateLimitMiddleware] }>) {
    return { ok: true };
  }
}

Overriding handle (or adding a subclass method) also gives you full control over key resolution — for example, keying on a user id instead of the client IP by overriding resolveKey in a subclass of RateLimitMiddleware before wrapping it.

Alternative: DI-resolved store via useFactory

If the store itself needs a DI-resolved dependency (a shared Storage binding, a request-scoped Context), register RateLimitMiddleware through a useFactory provider instead — the factory's parameter types are the dependency declaration, and the RateLimitPolicy literal is captured in the factory closure:

src/api/app-rate-limit.store.ts
import { MemoryRateLimitStore } from "@heximon/http/security";

export class AppRateLimitStore extends MemoryRateLimitStore {}
src/api/api.module.ts
import { Module } from "@heximon/runtime";
import { TimeSpan } from "@heximon/primitives";
import { RateLimitMiddleware } from "@heximon/http/security";
import { AppRateLimitStore } from "./app-rate-limit.store";
import { ApiController } from "./api.controller";

export class ApiModule extends Module({
  providers: [
    AppRateLimitStore,
    {
      provide: RateLimitMiddleware,
      useFactory: (store: AppRateLimitStore): RateLimitMiddleware =>
        new RateLimitMiddleware(store, { limit: 100, window: new TimeSpan(1, "m") }),
    },
  ],
  http: {
    controllers: [ApiController],
    middlewares: [RateLimitMiddleware],
  },
}) {}

A rate-limited request receives a 429 response with a Retry-After header.

MemoryRateLimitStore is per-isolate. State lives in the process's memory, so two Cloudflare Workers isolates serving the same route do not share a counter. For distributed-correct rate limiting across isolates, use RateLimitRoomStore from @heximon/durable/security (see below), or implement RateLimitStore over any networked store. The port's hit(key, policy) method owns the algorithm and atomicity — swap the impl, not the middleware.

Distributed rate limiting — @heximon/durable/security {#distributed-rate-limiting}

MemoryRateLimitStore doesn't share state across isolates — that's a property of memory, not a bug.

When you need a global counter, @heximon/durable/security ships a turnkey RateLimitStore backed by a Cloudflare Durable Object: one single-threaded DO per rate-limit key, so the sliding-window counter is strongly consistent everywhere. The port is the swap seam — RateLimitMiddleware, its policy, and the controller stay identical; only the store provider and two durable module entries change:

src/api/api.module.ts
import { Module } from "@heximon/runtime";
import { TimeSpan } from "@heximon/primitives";
import { RateLimitRoom, RateLimitRoomFactory, RateLimitRoomStore } from "@heximon/durable/security";
import { RateLimitMiddleware } from "@heximon/http/security";
import { SecurityController } from "./security.controller";

export class ApiModule extends Module({
  providers: [
    RateLimitRoomStore,
    {
      provide: RateLimitMiddleware,
      useFactory: (store: RateLimitRoomStore) =>
        new RateLimitMiddleware(store, { limit: 5, window: new TimeSpan(30, "s") }),
    },
  ],
  http: {
    controllers: [SecurityController],
    middlewares: [RateLimitMiddleware],
  },
  durable: {
    objects: [RateLimitRoom],
    factories: [RateLimitRoomFactory],
  },
}) {}

The subpath ships the whole Durable Object tier — RateLimitRoom, its RateLimitRoomFactory, and the concrete RateLimitRoomStore — so the app lists them with no local Durable Object shells; the compiler recovers the DO's RPC surface and the factory's bound DO from the package's shipped .d.ts.

One DO per key caps throughput at ~500–1,000 req/s per key — RateLimitRoomStore exposes protected sharding hooks to trade an exact global limit for ~N× throughput on hot keys.

See Durable Objects for how a listed DO is constructed and wired, and Deploy to Cloudflare for registering DurablePlugin and the generated wrangler.json bindings.

CSRF posture

Heximon's built-in auth is bearer-token-only (Authorization: Bearer <token>). Classic CSRF attacks work by exploiting ambient credentials (cookies, <form>-submittable headers) that a browser sends automatically to a target origin. Bearer tokens are never sent automatically — a cross-origin page cannot read them from another origin's memory, so it cannot forge a request that carries one.

If your app adds cookie-based sessions, wire a CSRF middleware appropriate for that flow. The bearer-only design means no CSRF middleware is needed for the default JWTAuthMiddleware setup.

See also

  • Authentication — the JWTAuthMiddleware and GuardMiddleware that pair with the security headers.
  • Middleware — how to attach middleware per controller or module-wide.
  • Security hardening — the body guard, response headers, and a rate-limited endpoint exercised with curl.
Copyright © 2026