Coherent.js Security Guide
This guide covers the security features built into Coherent.js — the renderer, the @coherent.js/api router and the other packages — and how to use them safely.
Table of Contents
- Rendering and XSS
- Authentication & Authorization
- Input Validation
- Rate Limiting & DoS Protection
- Security Headers
- CORS Configuration
- Request Size Limits
- Error Messages
- Password Security
- CSRF Protection
- Database Queries
- Development Server
- Security Testing
Rendering and XSS
@coherent.js/core escapes by default:
textand every attribute value are HTML-escaped.- Attribute names are validated: a name containing whitespace, quotes,
<,>,/,=or control characters makesrender()throw, so spreading request data into props cannot inject an attribute or break out of a tag. - Function-valued
on*props render nothing on the server. - The text of a
<script>or<style>element cannot close the element.
Raw HTML only goes through two explicit doors, html: and dangerouslySetInnerContent():
import { dangerouslySetInnerContent } from '@coherent.js/core';
{ div: { html: sanitizedHtml } } // raw
{ div: { children: [dangerouslySetInnerContent(sanitizedHtml)] } } // rawMarkers from dangerouslySetInnerContent() carry a non-enumerable symbol brand. A plain object such as { "__html": "<img onerror=...>", "__trusted": true } — for example from a JSON request body — is never treated as trusted. Only pass HTML you produced or sanitized with a dedicated HTML sanitizer (a regex-based "escape" is not a sanitizer).
Related helpers:
@coherent.js/seowrites JSON-LD with<,>and&escaped, so structured data cannot swallow the page.@coherent.js/i18ncan escape interpolated params:createTranslator({ escape: true })ort(key, params, { escape: true }).text:is escaped by core anyway; this matters when you insert a translation throughhtml:.
Authentication & Authorization
JWT Authentication
@coherent.js/api has no default secret: withAuth(), generateJWT() and verifyToken() throw without one.
import { createRouter, withAuth, withRole, generateJWT } from '@coherent.js/api';
const secret = process.env.JWT_SECRET; // e.g. `openssl rand -hex 32`
const auth = withAuth({ secret }); // verifies Authorization: Bearer <jwt> (HS256)
// Issue a token (payload, expiresIn, secret)
const token = generateJWT({ sub: 123, role: 'user' }, '24h', secret);
const router = createRouter({
api: {
profile: {
GET: { middleware: [auth], handler: (req) => ({ user: req.user }) }
},
admin: {
GET: { middleware: [auth, withRole('admin')], handler: () => ({ message: 'Admin access granted' }) }
}
}
});Middleware that answers (401 from withAuth, 403 from withRole) stops the chain: the handler does not run. Signatures are compared in constant time, and verifyToken() returns null for an invalid, forged or expired token.
Custom Authentication
withAuth({ verify }) accepts any scheme; the verifier may be async and returns the user or null:
const apiKeyAuth = withAuth({
verify: async (req) => {
const apiKey = req.headers['x-api-key'];
return apiKey ? await getUserByApiKey(apiKey) : null; // null → 401
}
});Or throw an error class from middleware:
import { AuthenticationError } from '@coherent.js/api';
const requireApiKey = async (req) => {
if (!(await validateApiKey(req.headers['x-api-key']))) {
throw new AuthenticationError('Invalid API key'); // answered with 401
}
};Input Validation
import { createRouter } from '@coherent.js/api';
const userSchema = {
type: 'object',
properties: {
username: { type: 'string', minLength: 3, maxLength: 30, pattern: '^[a-zA-Z0-9_]+