Security Middleware
Security Features
Built-in security features like CORS, Helmet, and rate limiting. For advanced security, integrate with middleware and third-party libraries.
Overview
Enable security features with one line. CORS, Helmet, rate limiting, and input validation built-in.
1import { createApp } from '@morojs/moro';
2
3// Basic security setup (actual implementation)
4const app = await createApp({
5 cors: true, // Enable CORS
6 compression: true, // Enable gzip compression
7 helmet: true // Enable security headers
8});Without proper security, your API is vulnerable to attacks. With MoroJS, you get essential security features out of the box.
Traditional security setup requires multiple libraries and complex configuration. We handle that automatically.
Without Security
- Vulnerable to common attacks
- Manual security header management
- Complex CORS configuration
- No built-in rate limiting
With MoroJS
- Built-in security headers (Helmet)
- One-line CORS enablement
- Automatic input validation
- Per-route rate limiting
Add validation and rate limiting to any route. That's it.
1// Input validation with Zod (built-in)
2app.post('/users')
3 .body(z.object({
4 name: z.string().min(2).max(50),
5 email: z.string().email(),
6 password: z.string().min(8)
7 }))
8 .handler((req, res) => {
9 // Input is automatically validated and sanitized
10 const { name, email, password } = req.body;
11 return { success: true, data: { name, email } };
12 });
13
14// Rate limiting (chainable API)
15app.post('/auth/login')
16 .rateLimit({ requests: 5, window: 900000 }) // 5 attempts per 15 minutes
17 .handler((req, res) => {
18 return authenticateUser(req.body);
19 });Protected
Security headers, CORS, and input validation. Secure by default.
Flexible
Built-in features plus extensibility. Add advanced security when needed.
Simple
One-line enablement. Automatic configuration. Zero setup.
How It Works
MoroJS includes basic security features out of the box. CORS, Helmet security headers, compression, and input validation are available with simple configuration. For advanced security features, integrate with middleware libraries and implement custom security measures.
Built-in Security Features
1import { createApp } from '@morojs/moro';
2
3// Basic security setup (actual implementation)
4const app = await createApp({
5 cors: true, // Enable CORS
6 compression: true, // Enable gzip compression
7 helmet: true // Enable security headers
8});
9
10// Input validation with Zod (built-in)
11app.post('/users')
12 .body(z.object({
13 name: z.string().min(2).max(50),
14 email: z.string().email(),
15 password: z.string().min(8)
16 }))
17 .handler((req, res) => {
18 // Input is automatically validated and sanitized
19 const { name, email, password } = req.body;
20
21 // Hash password before storing (use bcrypt library)
22 const hashedPassword = hashPassword(password);
23
24 const user = createUser({
25 name,
26 email,
27 password: hashedPassword
28 });
29
30 return { success: true, data: user };
31 });
32
33// Rate limiting (chainable API)
34app.post('/auth/login')
35 .rateLimit({ requests: 5, window: 900000 }) // 5 attempts per 15 minutes
36 .handler((req, res) => {
37 return authenticateUser(req.body);
38 });What's Included
- CORS support (simple boolean flag)
- Helmet security headers
- Gzip compression
- Input validation with Zod
- Rate limiting (per-route)
- WebSocket support
Additional Security
For advanced security, integrate these libraries:
- •
bcrypt- Password hashing - •
jsonwebtoken- JWT authentication - •
express-rate-limit- Advanced rate limiting - •
express-validator- Additional validation - •
passport- Authentication strategies
Advanced Security Configuration
For advanced security features, you can configure comprehensive security headers, implement authentication and authorization, set up rate limiting and DDoS protection, and add security monitoring.
1import { createApp } from '@morojs/moro';
2
3const app = await createApp({
4 security: {
5 helmet: {
6 // Content Security Policy
7 contentSecurityPolicy: {
8 directives: {
9 defaultSrc: ["'self'"],
10 scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.example.com"],
11 styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
12 imgSrc: ["'self'", "data:", "https:"],
13 connectSrc: ["'self'", "https://api.example.com"],
14 fontSrc: ["'self'", "https://fonts.gstatic.com"],
15 objectSrc: ["'none'"],
16 mediaSrc: ["'self'"],
17 frameSrc: ["'none'"],
18 },
19 reportOnly: false, // Set to true for testing
20 reportUri: '/csp-report'
21 },
22
23 // HTTP Strict Transport Security
24 hsts: {
25 maxAge: 31536000, // 1 year
26 includeSubDomains: true,
27 preload: true
28 },
29
30 // X-Frame-Options
31 frameguard: {
32 action: 'deny' // or 'sameorigin'
33 },
34
35 // X-Content-Type-Options
36 noSniff: true,
37
38 // X-XSS-Protection
39 xssFilter: true,
40
41 // Referrer Policy
42 referrerPolicy: { policy: "strict-origin-when-cross-origin" },
43
44 // Hide X-Powered-By header
45 hidePoweredBy: true,
46
47 // DNS Prefetch Control
48 dnsPrefetchControl: { allow: false },
49
50 // IE No Open
51 ieNoOpen: true,
52
53 // Cross-Origin Embedder Policy
54 crossOriginEmbedderPolicy: true,
55
56 // Cross-Origin Opener Policy
57 crossOriginOpenerPolicy: { policy: "same-origin" },
58
59 // Cross-Origin Resource Policy
60 crossOriginResourcePolicy: { policy: "cross-origin" },
61
62 // Origin Agent Cluster
63 originAgentCluster: true,
64
65 // Permitted Cross-Domain Policies
66 permittedCrossDomainPolicies: false
67 }
68 }
69});1const getSecurityConfig = () => {
2 const isProd = process.env.NODE_ENV === 'production';
3
4 return {
5 helmet: {
6 contentSecurityPolicy: {
7 directives: {
8 defaultSrc: ["'self'"],
9 scriptSrc: isProd
10 ? ["'self'"]
11 : ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
12 styleSrc: ["'self'", "'unsafe-inline'"],
13 imgSrc: ["'self'", "data:", "https:"],
14 connectSrc: isProd
15 ? ["'self'", "https://api.example.com"]
16 : ["'self'", "http://localhost:*", "ws://localhost:*"]
17 },
18 reportOnly: !isProd
19 },
20 hsts: isProd ? {
21 maxAge: 31536000,
22 includeSubDomains: true,
23 preload: true
24 } : false
25 }
26 };
27};
28
29const app = await createApp({
30 security: getSecurityConfig()
31});Authentication & Authorization
1import {
2 createApp, auth, providers, requireRole, requirePermission
3} from '@morojs/moro';
4
5const app = await createApp();
6
7// JWT is a session strategy on the auth middleware, not a separate import.
8app.use(auth({
9 secret: process.env.AUTH_SECRET!,
10 providers: [
11 providers.credentials({
12 authorize: async credentials => verifyUser(credentials)
13 })
14 ],
15 session: {
16 strategy: 'jwt',
17 maxAge: 60 * 60 // seconds
18 },
19 jwt: {
20 secret: process.env.JWT_SECRET,
21 maxAge: 60 * 60
22 }
23}));
24
25// Role-based access control
26app.post('/admin/users')
27 .use(requireRole(['admin', 'super-admin']))
28 .handler(createUser);
29
30// Permission-based access control
31app.delete('/posts/:id')
32 .use(requirePermission('posts:delete'))
33 .handler(deletePost);1import { createApp, auth, providers } from '@morojs/moro';
2
3const app = await createApp();
4
5// Providers handle the full OAuth flow — authorization redirect, code
6// exchange and profile fetch — so there are no routes to write yourself.
7app.use(auth({
8 secret: process.env.AUTH_SECRET!,
9 providers: [
10 providers.google({
11 clientId: process.env.GOOGLE_CLIENT_ID!,
12 clientSecret: process.env.GOOGLE_CLIENT_SECRET!
13 }),
14 providers.github({
15 clientId: process.env.GITHUB_CLIENT_ID!,
16 clientSecret: process.env.GITHUB_CLIENT_SECRET!
17 })
18 ],
19 callbacks: {
20 // Persist your own user record on first sign-in
21 signIn: async ({ user, account }) => {
22 await createOrUpdateUser(user, account);
23 return true;
24 },
25 session: async ({ session, token }) => {
26 session.user.id = token.sub;
27 return session;
28 }
29 }
30}));
31
32// Built-in providers: google, github, discord, twitter, microsoft, apple,
33// facebook, linkedin, credentials, email, magicLink, otp, passkeyRate Limiting & DDoS Protection
1import { createApp, middleware } from '@morojs/moro';
2
3const app = await createApp();
4
5// Limits are tracked per client address and per "METHOD:path" route key.
6app.use(middleware.rateLimit({
7 windowMs: 15 * 60 * 1000, // 15 minutes
8 max: 1000, // per client, per route, per window
9 message: 'Too many requests from this IP, please try again later.'
10}));Before you rely on it
- The option surface is { requests | max, window | windowMs, message, statusCode, skipSuccessfulRequests, skipFailedRequests } — Express-style keyGenerator, skip, handler and store options are not part of it
- Counting is in-process and keyed by client IP and route, so each worker enforces its own limit independently
- Scope limits per route, not by mounting at a prefix — app.use('/api/', ...) is silently ignored, since a path prefix is only honoured for createRouter() instances
- Exceeding the limit returns the configured status (429 by default) with a Retry-After header
1// Route-level rate limiting is configured on the route itself,
2// not by mounting middleware at a path prefix.
3// RateLimitConfig = { requests, window, skipSuccessfulRequests? }
4app.post('/auth/login')
5 .rateLimit({ requests: 5, window: 15 * 60 * 1000, skipSuccessfulRequests: true })
6 .handler(handler);
7
8// Limits are keyed by client IP + method:path, and run before auth and
9// validation - an over-limit client is rejected without any token
10// verification or schema parsing being done on its behalf.1import { createApp, middleware } from '@morojs/moro';
2
3// Receive-phase timeouts bound slow-read and slow-header attacks.
4const app = await createApp({
5 server: {
6 timeouts: {
7 request: 30_000, // full-request receive budget; does not reset on activity
8 idle: 10_000, // socket inactivity
9 headers: 6_000, // headers-received deadline
10 keepAlive: 5_000
11 }
12 }
13});
14
15// Cap request body size
16app.use(middleware.bodySize({ limit: '1mb' }));
17
18// Cap request volume per client, per route
19app.use(middleware.rateLimit({ windowMs: 60_000, max: 300 }));Scope of these controls
- These are per-process application-layer limits — they bound what one instance will accept
- Volumetric and connection-flood defence belongs upstream, at your CDN, load balancer or WAF
- IP allow/deny lists are not built in; enforce them at the edge or in your own middleware
Input Validation & Sanitization
1import { z } from '@morojs/moro';
2
3// Sanitize inside the schema with a transform. MoroJS has no separate
4// sanitizer, so use a dedicated library for HTML (e.g. sanitize-html)
5// and keep the escaping decision at the point of output.
6const SanitizedString = z.string().transform(val =>
7 val.replace(/<[^>]*>/g, '').trim()
8);
9
10const EmailSchema = z.string()
11 .email('Invalid email format')
12 .transform((val) => val.toLowerCase().trim());
13
14const PasswordSchema = z.string()
15 .min(8, 'Password must be at least 8 characters')
16 .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
17 'Password must contain uppercase, lowercase, number, and special character');
18
19// User registration with validation
20const CreateUserSchema = z.object({
21 email: EmailSchema,
22 password: PasswordSchema,
23 name: SanitizedString.min(2).max(50),
24 bio: SanitizedString.max(500).optional(),
25 website: z.string().url().optional().transform((val) => {
26 // Ensure HTTPS for external links
27 if (val && !val.startsWith('https://')) {
28 return `https://${val.replace(/^https?:\/\//, '')}`;
29 }
30 return val;
31 })
32});
33
34// Attach the schema with the chainable route builder.
35// A failed body validation responds 400 with per-field details.
36app.post('/users')
37 .body(CreateUserSchema)
38 .handler(async (req, res) => {
39 const { email, password, name, bio, website } = req.body;
40
41 if (await isEmailBlacklisted(email)) {
42 return res.status(403).json({ error: 'Email domain not allowed' });
43 }
44
45 const user = await createUser({
46 email,
47 password: await bcrypt.hash(password, 12),
48 name,
49 bio,
50 website
51 });
52
53 res.json({ id: user.id, email: user.email, name: user.name });
54 });1import { createDatabaseAdapter, z } from '@morojs/moro';
2
3const db = createDatabaseAdapter('postgresql', {
4 connectionString: process.env.DATABASE_URL
5});
6await db.connect();
7
8// Pass values as parameters — never interpolate them into the SQL string.
9app.get('/users/:id')
10 .params(z.object({ id: z.string().uuid('Invalid user ID format') }))
11 .handler(async (req, res) => {
12 const user = await db.queryOne(
13 'SELECT id, email, name FROM users WHERE id = $1',
14 [req.params.id]
15 );
16 res.json(user);
17 });
18
19// Search: validate and bound the input, still parameterize the query
20app.get('/users/search')
21 .query(z.object({
22 q: z.string().min(2, 'Search query too short').max(100, 'Search query too long')
23 }))
24 .handler(async (req, res) => {
25 const results = await db.query(
26 'SELECT id, name, email FROM users ' +
27 'WHERE search_vector @@ plainto_tsquery($1) LIMIT 20',
28 [req.query.q]
29 );
30 res.json(results);
31 });Security Monitoring & Logging
1import { createFrameworkLogger } from '@morojs/moro';
2
3const securityLogger = createFrameworkLogger('Security');
4
5// Security event middleware
6const securityMonitoring = (req, res, next) => {
7 // Log suspicious patterns
8 const suspiciousPatterns = [
9 /\.\.\//, // Directory traversal
10 /<script/i, // XSS attempts
11 /union.*select/i, // SQL injection
12 /eval\s*\(/i, // Code injection
13 /javascript:/i // JavaScript protocol
14 ];
15
16 const userAgent = req.get('User-Agent') || '';
17 const url = req.url;
18 const body = JSON.stringify(req.body);
19
20 // Check for suspicious patterns
21 const suspicious = suspiciousPatterns.some(pattern =>
22 pattern.test(url) || pattern.test(body) || pattern.test(userAgent)
23 );
24
25 if (suspicious) {
26 // warn(message, context, metadata)
27 securityLogger.warn('Suspicious request detected', 'RequestScan', {
28 ip: req.ip,
29 userAgent,
30 url,
31 method: req.method,
32 severity: 'HIGH'
33 });
34
35 // Optional: Block the request
36 // return res.status(400).json({ error: 'Request blocked' });
37 }
38
39 // Log failed authentication attempts
40 res.on('finish', () => {
41 if (req.path.includes('/auth/') && res.statusCode === 401) {
42 securityLogger.warn('Failed authentication attempt', 'Auth', {
43 ip: req.ip,
44 userAgent,
45 path: req.path
46 });
47 }
48 });
49
50 next();
51};
52
53app.use(securityMonitoring);1import { createApp, createFrameworkLogger } from '@morojs/moro';
2
3const app = await createApp();
4const log = createFrameworkLogger('Security');
5
6// Build alerting on the event bus. Emit your own events from the
7// middleware above, then subscribe and fan out to your alert channel.
8const failures = new Map();
9
10// Listeners receive an EventPayload — { context, data }, not the raw value.
11app.events.on('auth:failed', async ({ data }) => {
12 const { ip } = data;
13 const count = (failures.get(ip) ?? 0) + 1;
14 failures.set(ip, count);
15
16 if (count >= 5) {
17 log.error('Repeated failed logins', 'Alerting', { ip, count });
18 await fetch(process.env.SECURITY_WEBHOOK_URL!, {
19 method: 'POST',
20 headers: { 'content-type': 'application/json' },
21 body: JSON.stringify({ severity: 'HIGH', ip, attempts: count })
22 });
23 failures.delete(ip);
24 }
25});
26
27// Emit from wherever you detect the condition:
28// await app.events.emit('auth:failed', { ip: req.ip });Rolling your own
- MoroJS ships the event bus and logger; threshold tracking and alert delivery are yours to build
- Counters held in process memory are per-worker — back them with Redis if you run more than one
- There is no built-in email, Slack or webhook alert channel
Security Best Practices
Security Checklist
- Use HTTPS in production
- Validate and sanitize all inputs
- Implement proper authentication
- Use parameterized queries
- Set security headers
- Enable rate limiting
- Monitor security events
- Keep dependencies updated
Common Vulnerabilities
- SQL Injection
- Cross-Site Scripting (XSS)
- Cross-Site Request Forgery (CSRF)
- Insecure Direct Object References
- Security Misconfiguration
- Broken Authentication
- Sensitive Data Exposure
- Insufficient Logging & Monitoring
1// Production security configuration
2const productionSecurityConfig = {
3 // Force HTTPS
4 server: {
5 https: {
6 enabled: true,
7 redirectHttp: true,
8 hsts: {
9 maxAge: 31536000,
10 includeSubDomains: true,
11 preload: true
12 }
13 }
14 },
15
16 // Security headers
17 helmet: {
18 contentSecurityPolicy: {
19 directives: {
20 defaultSrc: ["'self'"],
21 scriptSrc: ["'self'"],
22 styleSrc: ["'self'", "'unsafe-inline'"],
23 imgSrc: ["'self'", "data:", "https:"],
24 connectSrc: ["'self'"],
25 fontSrc: ["'self'"],
26 objectSrc: ["'none'"],
27 mediaSrc: ["'self'"],
28 frameSrc: ["'none'"]
29 }
30 },
31 frameguard: { action: 'deny' },
32 noSniff: true,
33 xssFilter: true,
34 referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
35 },
36
37 // Rate limiting
38 rateLimit: {
39 global: { max: 1000, window: '15m' },
40 auth: { max: 5, window: '15m' },
41 api: { max: 100, window: '15m' }
42 },
43
44 // CORS
45 cors: {
46 origin: ['https://yourdomain.com'],
47 credentials: true,
48 methods: ['GET', 'POST', 'PUT', 'DELETE'],
49 allowedHeaders: ['Content-Type', 'Authorization']
50 },
51
52 // Authentication
53 auth: {
54 jwt: {
55 secret: process.env.JWT_SECRET, // Use strong secret
56 algorithm: 'HS256',
57 expiresIn: '1h'
58 }
59 },
60
61 // Logging
62 logging: {
63 level: 'info',
64 security: {
65 enabled: true,
66 logFailedAttempts: true,
67 logSuspiciousActivity: true
68 }
69 }
70};