ExisJS

Security and Traffic Control

ExisJS includes built-in security and traffic control middlewares in exisjs/middleware and security decorators in exisjs/decorators. These components protect against common web vulnerabilities, brute-force requests, parameter pollution, and scanner abuse.


Security Middleware Reference

The following security middlewares are exported from exisjs/middleware and can be registered globally in server.ts, in exis.config.ts, or scoped to route hierarchies in boundary.ts:

MiddlewareSignatureDescription
helmethelmet(options?)Sets secure HTTP headers (Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, Content-Security-Policy, etc.).
csrfcsrf(options)Protects state-changing requests (POST, PUT, DELETE, PATCH) using the Signed Double-Submit Cookie pattern.
rateLimitrateLimit(options?)Enforces request volume limits per IP or custom key, returning 429 Too Many Requests with standard rate-limit headers.
ipFilteripFilter(options)Grants or restricts access based on client IP addresses and CIDR subnets.
idempotentidempotent(options?)Caches mutation responses keyed by the Idempotency-Key header to prevent duplicate operations.
hpphpp()Protects against HTTP Parameter Pollution by normalizing repeated query parameters and form fields.
mongoSanitizemongoSanitize()Strips MongoDB query operator keys (keys starting with $ or containing .) from bodies, queries, and parameters.
timeouttimeout(ms | options)Terminates requests exceeding a specified duration with an HTTP 503 Service Unavailable response.
blockSuspiciousProbesblockSuspiciousProbes(options?)Intercepts automated vulnerability scanners (probing for .env, /.git, WordPress files) and terminates them immediately.
timingSafeEqualtimingSafeEqual(a, b)Compares secrets, tokens, or hashes in constant time to prevent side-channel timing attacks.

1. Security Headers (helmet)

Helmet is on by default (helmet: { enabled: true } in exis.config.ts), including a same-origin Content-Security-Policy; see Security Baseline. Use the middleware directly to apply different settings to part of the app.

The helmet middleware sets secure response headers to harden the application against cross-site scripting (XSS), clickjacking, and content sniffing:

src/http/server.ts
import { exis } from 'exisjs'import { helmet } from 'exisjs/middleware'
export default exis({  async onStart(app) {    app.use(      helmet({        hsts: { maxAge: 31536000, includeSubDomains: true },        frameguard: { action: 'DENY' },        noSniff: true,        xssFilter: true,        contentSecurityPolicy: "default-src 'self'",      })    )  },})

2. CSRF Protection (csrf)

ExisJS implements the Signed Double-Submit Cookie pattern for CSRF defense:

  • On safe requests (GET, HEAD, OPTIONS), a cryptographically signed cookie is issued to the client.
  • On state-changing requests (POST, PUT, DELETE, PATCH), the client must supply the unsigned token in the x-csrf-token header.
src/http/server.ts
import { exis } from 'exisjs'import { csrf } from 'exisjs/middleware'
export default exis({  async onStart(app) {    app.use(      csrf({        secret: process.env.CSRF_SECRET!, // Must be at least 32 characters        cookieName: 'csrf-token',        headerName: 'x-csrf-token',        cookieOptions: {          secure: process.env.NODE_ENV === 'production',          sameSite: 'Lax',        },      })    )  },})

3. Rate Limiting (rateLimit)

Use rateLimit to prevent brute-force attacks and abuse by capping incoming requests over a sliding time window.

src/http/boundary.ts
import { defineBoundary } from 'exisjs/router'import { rateLimit } from 'exisjs/middleware'
export const limiter = rateLimit({  windowMs: 60 * 1000, // 1 minute  max: 100, // Limit each IP to 100 requests per windowMs  message: 'Too many requests from this IP, please try again after a minute',  keyGenerator: (req) => req.ip,})
export const config = defineBoundary({  middleware: [limiter],})

The default store counts per process: with two instances each allows max, and on serverless the count resets constantly. Pass a shared store to enforce one limit everywhere:

src/http/auth/boundary.ts
import { rateLimit, type RateLimitStore } from 'exisjs/middleware'import { redis } from '../../lib/redis'
const redisStore: RateLimitStore = {  async increment(key, windowMs) {    const k = `rl:${key}`    const [[, count], [, ttl]] = (await redis      .multi()      .incr(k)      .pexpire(k, windowMs, 'NX')      .pttl(k)      .exec()) as [unknown, number][]    return { count, resetTime: Date.now() + Math.max(ttl, 0) }  },}
export const loginLimiter = rateLimit({ max: 10, windowMs: 15 * 60_000, store: redisStore })

If the store fails, requests are let through and a warning is logged, so a cache outage does not take the API down.

Rate Limit Response Headers

rateLimit attaches standard IETF and legacy rate-limiting headers to every response:

HeaderDescriptionExample
RateLimit-LimitRequest quota allocated for the configured window.100
RateLimit-RemainingRemaining requests permitted within the active window.42
RateLimit-ResetTime in seconds until the current quota window resets.18
Retry-AfterIncluded on 429 Too Many Requests responses, indicating wait time.18
X-RateLimit-*Legacy header equivalents provided for compatibility.X-RateLimit-Reset: 1727871234

4. IP Filtering and CIDR Subnet Control (ipFilter)

Restrict access to specific IP addresses or CIDR ranges using ipFilter:

src/http/admin/boundary.ts
import { defineBoundary } from 'exisjs/router'import { ipFilter } from 'exisjs/middleware'
export const adminIpGuard = ipFilter({  allow: [    '127.0.0.1',    '10.0.0.0/8', // Internal VPN subnet    '192.168.1.0/24',  ],  deny: [    '10.0.0.99', // Block specific compromised host  ],  message: 'Access Denied: IP address not authorized',})
export const config = defineBoundary({  middleware: [adminIpGuard],})

5. Idempotency Support (idempotent)

Prevent duplicate mutations (e.g. double-charging a credit card or duplicate database inserts) by caching responses associated with an Idempotency-Key header:

  • A repeat with the same key, user, method, path and body gets the first response again (header Idempotent-Replayed: true); the handler does not run twice.
  • Keys belong to req.user.id (or sub), so one client can never receive another client's response. Change this with scope: (req) => req.tenantId.
  • The same key with a different body is refused with 422 IDEMPOTENCY_KEY_REUSED; a repeat that arrives while the first is still running gets 409 IDEMPOTENCY_IN_PROGRESS.
  • Only 2xx/3xx responses are stored, so a failed request can be retried. Pass store to share responses between instances.
src/http/checkout/route.ts
import { controller, route } from 'exisjs/router'import { idempotent } from 'exisjs/middleware'
export default controller({  charge: route.post('/', {    middleware: [      idempotent({        headerName: 'Idempotency-Key',        ttlMs: 24 * 60 * 60 * 1000, // Cache for 24 hours      }),    ],    async handle({ body, res }) {      const charge = await paymentService.createCharge(body)      res.status(201).json({ success: true, charge })    },  }),})

6. Parameter Pollution and NoSQL Injection Protection

HTTP Parameter Pollution (hpp)

Prevents parameter pollution attacks where query string or body keys are duplicated (e.g. ?role=user&role=admin):

src/http/server.ts
import { exis } from 'exisjs'import { hpp } from 'exisjs/middleware'
export default exis({  async onStart(app) {    app.use(hpp())  },})

NoSQL Injection Sanitization (mongoSanitize / dbSanitize)

Strips prohibited operator keys containing $ or . from req.body, req.query, and req.params:

src/http/server.ts
import { exis } from 'exisjs'import { mongoSanitize } from 'exisjs/middleware'
export default exis({  async onStart(app) {    app.use(mongoSanitize())  },})

7. Request Timeout Control (timeout)

Set maximum request duration thresholds to avoid hung connections:

src/http/server.ts
import { exis } from 'exisjs'import { timeout } from 'exisjs/middleware'
export default exis({  async onStart(app) {    app.use(      timeout({        ms: 30000, // 30 seconds        statusCode: 503,        message: 'Request timed out',        exclude: ['/upload', '/stream'],      })    )  },})

8. Bot Probe Blackholing (blockSuspiciousProbes)

Automated vulnerability bots frequently scan applications for sensitive files (.env, /.git, wp-login.php, actuator/heapdump). Use blockSuspiciousProbes to intercept and terminate these requests immediately without polluting production application logs:

src/http/server.ts
import { exis } from 'exisjs'import { blockSuspiciousProbes } from 'exisjs/middleware'
export default exis({  async onStart(app) {    app.use(      blockSuspiciousProbes({        blackhole: true, // Terminates connection immediately with empty body        statusCode: 404,        silent: true, // Suppresses 404 noise from standard logger        patterns: [/^\/admin-backup/i],      })    )  },})

9. Timing-Safe Comparison (timingSafeEqual)

Use timingSafeEqual from exisjs/middleware when validating API tokens, signatures, or webhook secrets to guard against timing attacks:

src/common/guards/api-key.guard.ts
import { timingSafeEqual } from 'exisjs/middleware'import { UnauthorizedException } from 'exisjs/error'
export function apiKeyGuard(req: any) {  const providedKey = req.headers['x-api-key']  const expectedKey = process.env.API_KEY
  if (!timingSafeEqual(providedKey, expectedKey)) {    throw new UnauthorizedException('Invalid API Key')  }}

10. Route Authorization (@Roles, @Permissions, @Public)

Declare who may call a route; ExisJS checks it against req.user before guards, pipes and the handler run. Your authentication middleware (in a boundary or @Use) verifies the token and sets req.user.

src/http/users/controller.ts
import { Controller, Get, Post, Roles, Permissions, Public } from 'exisjs/decorators'
@Controller('/users')@Roles('admin', 'manager')export class UsersController {  @Get('/')  @Permissions('users:read')  async listUsers() {    return []  }
  @Post('/login')  @Public()  async login() {    return { token: 'jwt' }  }}

Functional routes declare the same with roles, permissions and public on the route (or on the whole controller({...})):

src/http/reports/route.ts
export default controller({  export: route.get('/export', {    permissions: ['report:read', 'report:export'],    handle: () => buildReport(),  }),})
RuleBehaviour
No req.user401 UNAUTHORIZED
@Roles('a', 'b')The user needs one of the roles, from user.roles (array) or user.role
@Permissions('x', 'y')The user needs all listed permissions in user.permissions. A role name never counts as a permission
Rule not met403 FORBIDDEN, standard error envelope
@Public()Wins over @Roles / @Permissions on the same route; a warning is logged at startup

There is no built-in "super admin" field. To let some users through every rule, say so explicitly:

exis.config.ts
export default {  authorization: { superUser: (user) => user.role === 'owner' },}

Deny by Default

By default a controller route without @Roles, @Permissions or @Public is open, so a forgotten decorator exposes the route. Turn that around and require a signed-in user everywhere except routes marked @Public():

exis.config.ts
export default {  authorization: { defaultPolicy: 'authenticated' },}

It applies to class and functional controllers; plain app.get() routes such as health checks are not affected.

exisjs routes prints the rule of every route in its ACCESS column, which is the quickest way to review what is public.


11. Security Baseline

What a new app sends and hides without extra code:

AreaDefault
Headers (helmet: { enabled: true })Content-Security-Policy: default-src 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'; form-action 'self', Strict-Transport-Security, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, X-XSS-Protection: 0; no X-Powered-By. Set contentSecurityPolicy to a string to change the policy or to false to send none. A policy set earlier by your own middleware is kept
/docsSends its own policy allowing the Swagger UI assets from jsDelivr. The project template enables /docs outside production only
ErrorsUnexpected errors answer INTERNAL_ERROR without message or stack outside development
Logs and validation errorsCredentials and connection strings are redacted (see Errors)
Request bodiesMalformed JSON is 400 INVALID_JSON; __proto__ / constructor keys are stripped

tex.string({ preventSql: true, escapeHtml: true }) is input hygiene, not an injection defence: always use parameterised queries, and encode output for the context it is written to (HTML, SQL, shell).