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
- What security misconfiguration covers
- Missing security headers in Express
- Verbose error messages and development mode in production
- Default accounts and values; unnecessary surface
- Docker container hardening
- XXE as parser configuration (deferred to 03-07)
- Process: reproducible, automated hardening
- Common mistakes, exercises and solutions
- 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.
- 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 |
- 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.
- Default accounts and values; unnecessary surface
- Default accounts: admin panels, databases and services often ship with
admin/adminor 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).
- 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:
| 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) |
- 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.
- 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/developmenton in production. SetNODE_ENV=productionand equivalents. - Containers as root and with the
latesttag. Use an unprivileged user and pinned versions. - Copying
.env/.gitinto 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 undefinedExercise 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
- OWASP Top Ten
- OWASP ASVS (Application Security Verification Standard)
- OWASP SAMM (Software Assurance Maturity Model)
- OWASP ZAP (Zed Attack Proxy)
- Other Key Projects: WSTG, Cheat Sheets and Dependency-Check
Module 3: OWASP Top Ten 2021 in Depth
- A01:2021 – Broken Access Control
- A02:2021 – Cryptographic Failures and Sensitive Data Exposure
- A03:2021 – Injection
- Cross-Site Scripting (XSS) in Depth
- A04:2021 – Insecure Design
- A05:2021 – Security Misconfiguration
- XML External Entities (XXE)
- A06:2021 – Vulnerable and Outdated Components
- A07:2021 – Identification and Authentication Failures
- A08:2021 – Software and Data Integrity Failures (Insecure Deserialization)
- A09:2021 – Security Logging and Monitoring Failures
- A10:2021 – Server-Side Request Forgery (SSRF)
Module 4: OWASP ASVS (Application Security Verification Standard)
Module 5: OWASP SAMM (Software Assurance Maturity Model)
Module 6: OWASP ZAP (Zed Attack Proxy)
- Introduction to ZAP
- Installation and Configuration
- Vulnerability Scanning
- Automating Security Testing
Module 7: Best Practices and Recommendations
- Secure Software Development Life Cycle (SDLC)
- Threat Modeling
- Integrating Security into DevOps (DevSecOps)
- Security Training and Awareness
- Additional Tools and Resources
Module 8: Practical Exercises and Case Studies
- Exercise 1: Identifying Vulnerabilities
- Exercise 2: Implementing Security Controls
- Case Study 1: Analyzing a Security Incident
- Case Study 2: Improving the Security of a Web Application
