Skip to content

JWT, Crypto & HTTP Security

The engine uses a custom C++ HTTP parser (not llhttp) and a native addon. Read Trust before production. Use the latest release on the public internet (npm install velociradix).

JWT and AES-256-GCM helpers ship in this package. The C++ layer rejects request smuggling, oversize headers, TRACE/CONNECT, and Slowloris-style incomplete requests before JavaScript runs.

Secrets for jwtAuth and session must come from the environment (process.env.JWT_SECRET), never from source.


1. Zero-Dependency JWT (jwtSign / jwtVerify)

Verification is constant-time (crypto.timingSafeEqual), blocks alg: none, and honors exp, nbf, iss, and aud.

js
import { createApp, jwtSign, jwtAuth } from "velociradix";

const app = createApp();
const JWT_SECRET = process.env.JWT_SECRET;

app.post("/login", (ctx) => {
  const token = ctx.jwtSign({ userId: 42, role: "admin" }, JWT_SECRET, {
    expiresIn: 3600,
    issuer: "velociradix",
  });
  return { token };
});

app.get(
  "/dashboard",
  (ctx) => {
    return { welcome: ctx.state.user };
  },
  {
    middlewares: [jwtAuth({ secret: JWT_SECRET, issuer: "velociradix" })],
  },
);

2. AES-256-GCM Encrypted Cookies & Sessions

IV is 12 bytes and the auth tag is 16 bytes. Malformed ciphertext returns undefined without leaking error details.

js
app.get("/login-session", (ctx) => {
  ctx.setEncryptedCookie(
    "user_session",
    { userId: 101, token: "abc" },
    process.env.COOKIE_SECRET,
  );
  return { status: "logged_in" };
});

app.get("/profile", (ctx) => {
  const session = ctx.getEncryptedCookie(
    "user_session",
    process.env.COOKIE_SECRET,
  );
  return { session };
});

setCookie() defaults to Path=/ and SameSite=Lax. Session cookies also set HttpOnly.


3. HTTP Parser Guarantees (C++ engine)

The parser is a custom C++ implementation, not llhttp. It closes the smuggling and DoS cases listed below; that is not the same as the operational maturity of a widely fuzzed library parser.

Applied to every connection before JavaScript runs:

CheckResult
HTTP/1.1 without Host400
Conflicting Content-Length400
Transfer-Encoding + Content-Length400
Header folding (obs-fold) / LF-only framing400
TRACE / CONNECT405
Header block > 32 KiB or > 100 headers431
URI > 8 KiB414
Incomplete headers idle > 10s408
CR/LF/NUL in response header names or valuesstripped

ctx.ip uses the TCP peer from accept() unless app.setTrustProxy(true) is set.


4. helmet() and cors()

js
app.use(helmet());
app.use(cors({ origin: "https://example.com", credentials: true }));

helmet() sends CSP, COOP, CORP, HSTS, X-Content-Type-Options, and Permissions-Policy. X-XSS-Protection is 0 (the legacy XSS auditor is harmful).

For a public API consumed from other origins:

js
app.use(
  helmet({
    contentSecurityPolicy: false,
    crossOriginResourcePolicy: "cross-origin",
  }),
);

cors({ credentials: true }) never pairs with Access-Control-Allow-Origin: * — the request Origin is reflected instead.

Native app.enableCors() / enable_cors() also never sends ACAO: *. It reflects Origin when present and omits ACAO otherwise. Prefer the JS cors({ origin: 'https://your.app' }) middleware in production so the allow-list is explicit.


5. CSRF

Double-submit cookie (httpOnly: false so JS can read it), SameSite=Strict, constant-time header compare, and Origin match after default-port normalization (example.comexample.com:443 on HTTPS). The token is also sent as X-CSRF-Token. Tokens are not accepted from the query string.

js
app.use(csrf());

6. What 8.2.0 did — and what it did not

8.1.x closed previously identified HTTP, JWT, path, and header issues. 8.2.0 covers the remaining known 8.1.1 findings:

  • Any Transfer-Encoding is rejected (chunked is not implemented).
  • Keep-alive idle connections close after 30s; incomplete headers still 408 after 10s.
  • JS↔C++ handles are napi_external tokens (slot + generation), not a heap address.
  • app.static() uses realpath + a prefix check.
  • app.ws() was removed (it was never a WebSocket). app.graphql() is POST-only with an 8 KiB query cap.
  • cache(), sizeLimit(), native CORS, CSRF, Set-Cookie lines, cookieParse, swagger/metrics/postman exposure, session flags, download/sendFile root, Content-Disposition, SSE framing, apiKey header-default, CLI JWT secret, and IPv6 accept are hardened as documented in this guide.

Still true: the HTTP parser is a custom C++ implementation, not llhttp. ctx.graphql() is a toy resolver, not a GraphQL server. Say the known 8.1.1 issues are covered — do not say security was “fully solved.”

Production vs local demo

  • Local demo: the library is usable as-is.
  • Internet production: use 8.2.0 or newer. Prefer JS cors({ origin }). Do not put cache() on authenticated routes without vary. Do not expose swagger / metricsUI / postmanDoc on the public internet unless { expose: true } on a private network. Use hostGuard({ hosts }) when setTrustProxy(true). Keep the custom parser in mind when threat-modeling.

Released under the MIT License.