You can have a solid design (A04) and injection-free code (A03), and still be exposed by how the system is configured and deployed. A05:2021 – Security Misconfiguration covers the flaws that don't live in your logic but in your settings: insecure defaults, factory accounts and passwords, error messages that reveal too much, missing security headers, unnecessary services and ports, loose permissions. It's one of the most frequent categories, precisely because modern software has hundreds of options and many ship "open for convenience".

In the 2021 Top Ten, this category absorbed the former XXE (XML External Entities), which is really just a misconfigured XML parser. We mention it here as part of A05, but because of its specific technique we cover it in depth in lesson 03-07. In BazarNube we'll review the configuration of the Express API and the Docker containers, two common sources of misconfiguration.

Legal and ethical notice: the examples are illustrative and use fake data. Practice only on systems you own or have explicit authorization to test.

Contents

  1. What security misconfiguration covers
  2. Missing security headers in Express
  3. Verbose error messages and development mode in production
  4. Default accounts and values; unnecessary surface
  5. Docker container hardening
  6. XXE as parser configuration (deferred to 03-07)
  7. Process: reproducible, automated hardening
  8. Common mistakes, exercises and solutions

  1. What security misconfiguration covers

A look at the variety of flaws that fall under A05:

Area Typical flaw Risk
HTTP headers Missing HSTS, X-Content-Type-Options, CSP XSS, sniffing, clickjacking
Errors Stack traces sent to the client Internal information leak
Accounts Default username/password left unchanged Trivial access
Services Exposed admin ports/panels Attack surface
Framework debug mode active in production Data leak, execution
Parsers XML with external entities enabled XXE (03-07)
Permissions Files/buckets with public access Data exposure

The common thread: defaults meant to "just work", not to "be secure", that were never hardened.

  1. Missing security headers in Express

The BazarNube API started up with no security headers and, worse, advertising its technology:

// app.js — VULNERABLE: no security headers, leaks X-Powered-By
const express = require('express');
const app = express();
// Express sends "X-Powered-By: Express" by default (a hint for the attacker)
app.get('/', (req, res) => res.send('BazarNube API'));

Every response announces X-Powered-By: Express (making it easy for an attacker to look up CVEs for that stack) and includes no defensive headers. Fix with helmet, which sets a sensible set of headers:

// app.js — SECURE: helmet applies security headers and hides the technology
const helmet = require('helmet');
app.disable('x-powered-by');            // stop revealing the framework
app.use(helmet());                       // HSTS, X-Content-Type-Options, etc.
app.use(helmet.contentSecurityPolicy({ directives: {
  defaultSrc: ["'self'"], scriptSrc: ["'self'"], frameAncestors: ["'none'"]
}}));
Header What it does Protects against
Strict-Transport-Security Forces HTTPS TLS downgrade (see A02)
X-Content-Type-Options: nosniff Prevents MIME type guessing Sniffing attacks
Content-Security-Policy Restricts script/resource origins XSS (see 03-04)
X-Frame-Options/frame-ancestors Prevents framing Clickjacking
Referrer-Policy Limits the Referer sent URL leakage

  1. Verbose error messages and development mode

In production, an error should log the detail on the server and return a generic message to the client. BazarNube had the opposite:

// VULNERABLE: sends the stack trace to the client
app.use((err, req, res, next) => {
  res.status(500).send(err.stack);   // reveals paths, versions, structure
});

A stack trace exposes file paths, library versions, table names and sometimes fragments of queries: a map for the attacker. Fix:

// SECURE: detailed internal log, generic response
app.use((err, req, res, next) => {
  logger.error({ err, path: req.path, reqId: req.id }); // detail ONLY in the log
  res.status(500).json({ error: 'Internal error', reqId: req.id });
});

The reqId lets support correlate the incident with the log without revealing anything to the client. Related: turn off the debug/development mode of frameworks in production (NODE_ENV=production), which often enables detailed error pages and disables security optimizations.

  1. Default accounts and values; unnecessary surface

  • Default accounts: admin panels, databases and services often ship with admin/admin or similar. Always change them; disable the ones you don't use.
  • Unnecessary services and ports: every exposed service is attack surface. If BazarNube doesn't need to expose the PostgreSQL port externally, don't publish it.
  • Diagnostic endpoints: metrics panels, /debug, internal API documentation... must be protected or disabled in production.
  • Directories and listings: turn off directory listing and don't serve configuration files (.env, .git).

  1. Docker container hardening

BazarNube runs on Docker. A careless Dockerfile is textbook misconfiguration:

# Dockerfile — VULNERABLE
FROM node:latest                 # floating tag: unpredictable version
WORKDIR /app
COPY . .                          # copies EVERYTHING, including .env and .git
RUN npm install
USER root                         # runs as root (by default)
CMD ["node", "app.js"]

Problems: base image with a floating tag (latest, impossible to reproduce and often carrying extra packages), copying secrets and Git history into the image, and running as root (if someone escapes the process, they're root in the container). Hardened version:

# Dockerfile — SECURE
FROM node:20.17-slim             # pinned version and slim variant (less surface)
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev            # reproducible install, no devDependencies
COPY src ./src                    # copy only what's needed (see .dockerignore)
RUN addgroup --system app && adduser --system --ingroup app app
USER app                          # run as an unprivileged user
CMD ["node", "src/app.js"]

And a .dockerignore to avoid leaking secrets or history:

.git
.env
node_modules
*.log
Practice Why
Pinned version tag Reproducibility; avoids silent changes
slim/minimal variant Fewer packages = fewer CVEs (links to A06, 03-08)
Non-root USER Limits the impact of a process escape
.dockerignore Avoids copying .env, .git, secrets
Scan the image Detects vulnerabilities in layers (image SCA)

  1. XXE as parser configuration

The former A04:2017 (XXE) was absorbed into A05 because, at its core, it's a misconfigured XML parser: it ships by default with external entity processing enabled, and nobody turned it off. Since it's a topic with its own technique and exploitation (file reading, SSRF, "billion laughs" DoS), we cover it in full in lesson 03-07 – XML External Entities (XXE). Remember the relationship: XXE is a specific case of security misconfiguration.

  1. Process: reproducible hardening

Secure configuration isn't a one-off adjustment, it's a repeatable process:

flowchart LR
  A[Hardening baseline] --> B[Config as code]
  B --> C[Automated scan in CI]
  C --> D[Same environment dev/staging/prod]
  D --> A
  • Hardening baseline: a reference list (e.g. CIS Benchmarks) of how each component should end up configured.
  • Configuration as code: define the config in versioned files (Dockerfile, IaC), not by hand on each server. That way it's auditable and reproducible.
  • Environment parity: dev, staging and production must be configured the same way; many breaches come from an "open" staging.
  • Automated scanning: tools that review headers, container config and IaC in the pipeline (we'll see this with ZAP in M6 for the web side).

Common Mistakes and Tips

  • Deploying with the default config. It almost always prioritizes "make it start" over "make it secure".
  • Revealing the technology (X-Powered-By, version headers, stack traces). Hide it.
  • Leaving debug/development on in production. Set NODE_ENV=production and equivalents.
  • Containers as root and with the latest tag. Use an unprivileged user and pinned versions.
  • Copying .env/.git into the image. Use .dockerignore.
  • Configuring each server by hand. It leads to inconsistencies; use configuration as code.
  • Tip: treat configuration with the same rigor as code: versioned, reviewed and scanned in CI.

Exercises

Exercise 1. List three security problems in this deployment fragment and fix them:

const app = express();
app.use(express.json());
app.use((err, req, res, next) => res.status(500).send(err.stack));
// no helmet, NODE_ENV undefined

Exercise 2. A colleague says: "hiding X-Powered-By is pointless, the attacker will find the technology anyway". How would you respond?

Exercise 3. Explain why running the container as root worsens the impact of another vulnerability (for example, a command execution like the one in A03).

Solutions

Solution 1. Problems: (1) no security headers → add helmet; (2) the error handler returns the stack to the client → log internally and respond generically; (3) NODE_ENV undefined → set it to production. Also, hide X-Powered-By. See sections 2 and 3.

Solution 2. It's true a determined attacker can infer the technology by other means, but hiding it raises the cost and slows the automated scanning that looks for specific versions to launch known exploits. It's defense in depth: not the main barrier, but it adds up and costs nothing.

Solution 3. If the process runs as root inside the container and a vulnerability allows running commands, those commands execute with maximum privileges: the attacker can modify any file in the container, install tools, and increase the odds of escaping to the host. With an unprivileged user, the blast radius is significantly reduced (least privilege, defense in depth).

Conclusion

A05 reminds us that security doesn't end with the code: defaults, headers, errors, accounts and containers must be hardened explicitly, repeatably and auditably. In BazarNube we added helmet and hid the technology, silenced stack traces to the client, and hardened the Dockerfile (pinned version, non-root user, .dockerignore). The key: configuration as code, reviewed just as thoroughly as the logic.

Backlog entry — A05: helmet + CSP enabled and X-Powered-By disabled; generic error handler with reqId; NODE_ENV=production; hardened Dockerfile (pinned tag, USER app, .dockerignore); config scanning in CI still pending.

We mentioned that XXE is a misconfigured XML parser. BazarNube's legacy Java module processes billing XML, so it's time to open that box. The next lesson is XML External Entities (XXE).

OWASP Course: Guidelines and Standards for Web Application Security

Module 1: Introduction to OWASP

Module 2: Main OWASP Projects

Module 3: OWASP Top Ten 2021 in Depth

Module 4: OWASP ASVS (Application Security Verification Standard)

Module 5: OWASP SAMM (Software Assurance Maturity Model)

Module 6: OWASP ZAP (Zed Attack Proxy)

Module 7: Best Practices and Recommendations

Module 8: Practical Exercises and Case Studies

Module 9: Assessment and Certification

© Copyright 2026. All rights reserved