← Blog
·11 min read

OWASP A05 Security Misconfiguration Guide

90% of apps have at least one security misconfiguration. Learn what OWASP A05:2021 covers, see vulnerable vs. secure Next.js code, and fix the most common gaps.

Rod

Founder & Developer

OWASP security misconfiguration is the most common finding across every application security benchmark. OWASP's own data from testing real applications puts it at 90% — nine out of ten apps have at least one misconfiguration. It moved from #6 to #5 in the OWASP Top 10 2021 and absorbed XXE (XML External Entity injection), which was its own separate category in 2017.

That tells you something: misconfigurations aren't edge cases. They're the default state of apps that haven't been explicitly hardened.

The good news is that most of them are easy to fix. The hard part is knowing they exist.


What OWASP A05:2021 Actually Covers

Security Misconfiguration is a broad category. OWASP defines it as any case where security could have been configured correctly but wasn't. That includes:

  • Missing or incorrectly configured security headers
  • Overly permissive CORS settings
  • Default credentials left active
  • Verbose error messages that expose internals to users
  • Unnecessary features, services, or ports left enabled
  • Source maps or debug endpoints accessible in production
  • XML parsers with external entity processing enabled (XXE)
  • API documentation or admin interfaces exposed publicly
  • GraphQL introspection enabled in production

The unifying thread: all of these are things that are off in a properly hardened app, but on by default in most frameworks and hosting environments.

What most articles miss: Vercel, Railway, Render, and Fly all deploy your app as-is. They don't add security headers. They don't disable GraphQL introspection. They don't strip source maps. That's your responsibility.

At Data Hogo, we scanned over 50 repositories across different stacks. Missing security headers showed up in 94% of them. Open CORS in API routes appeared in 31%. Source maps in production appeared in 27%. These aren't rare mistakes — they're the baseline.


The 9 Most Common Misconfigurations (With Fixes)

1. Missing Security Headers

HTTP security headers are instructions you send to the browser that tell it how to behave when loading your site. Without them, browsers use permissive defaults that expose users to clickjacking, MIME sniffing, cross-site scripting, and protocol downgrade attacks.

The headers that matter most:

Header What it prevents
Content-Security-Policy XSS by restricting which scripts can execute
X-Frame-Options Clickjacking by blocking your page from being iFramed
Strict-Transport-Security Protocol downgrade — forces HTTPS after first visit
X-Content-Type-Options MIME sniffing — forces browser to respect declared Content-Type
Referrer-Policy Leaking sensitive URLs to third parties via the Referer header
Permissions-Policy Restricts access to browser APIs (camera, mic, geolocation)

None of these are added by Next.js or Vercel by default. You add them in next.config.ts:

// next.config.ts — add this headers() function
import type { NextConfig } from "next";
 
const securityHeaders = [
  {
    key: "X-Frame-Options",
    value: "SAMEORIGIN", // blocks your site from being embedded in iframes
  },
  {
    key: "X-Content-Type-Options",
    value: "nosniff", // tells browser to trust declared Content-Type, don't guess
  },
  {
    key: "Strict-Transport-Security",
    value: "max-age=63072000; includeSubDomains; preload", // 2 years, HTTPS only
  },
  {
    key: "Referrer-Policy",
    value: "strict-origin-when-cross-origin",
  },
  {
    key: "Permissions-Policy",
    value: "camera=(), microphone=(), geolocation=()", // deny unless explicitly needed
  },
  {
    key: "Content-Security-Policy",
    // Start with a strict policy, loosen only what your app actually needs
    value: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';",
  },
];
 
const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: "/(.*)", // apply to every route
        headers: securityHeaders,
      },
    ];
  },
};
 
export default nextConfig;

Check your current headers free with Data Hogo's header checker — paste your URL and get an instant grade.

For a deeper dive on each header and how to tune CSP for Next.js specifically, see Next.js security headers — complete configuration guide.

You can also read the dedicated encyclopedia entries for missing Content-Security-Policy and missing X-Frame-Options to understand how each one is exploited.


2. CORS Set to Wildcard

CORS (Cross-Origin Resource Sharing) controls which websites can make requests to your API from a browser. When you set Access-Control-Allow-Origin: *, you're telling the browser: "Any website on the internet can call this endpoint."

For a public, read-only API that serves non-sensitive data, that's fine. For any API route that handles authentication, user data, or mutations, it's a vulnerability. It allows a malicious website to make requests to your API on behalf of your logged-in users without them knowing.

// BAD: app/api/user/route.ts — wildcard allows any origin
export async function GET(req: Request) {
  return Response.json(
    { user: await getCurrentUser() },
    {
      headers: {
        "Access-Control-Allow-Origin": "*", // any site can call this on your users' behalf
      },
    }
  );
}
// GOOD: restrict to your own domain (or a known list of trusted origins)
const ALLOWED_ORIGINS = [
  "https://datahogo.com",
  "https://app.datahogo.com",
];
 
export async function GET(req: Request) {
  const origin = req.headers.get("origin") ?? "";
  const allowedOrigin = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
 
  return Response.json(
    { user: await getCurrentUser() },
    {
      headers: {
        "Access-Control-Allow-Origin": allowedOrigin, // explicit allowlist
        "Vary": "Origin", // tells CDNs to cache per-origin
      },
    }
  );
}

See the full breakdown in our encyclopedia entry on open CORS in API routes.


3. Source Maps in Production

Source maps are files that map your minified, compiled JavaScript back to your original source code. They're essential in development — they make error messages readable. In production, they hand attackers a copy of your unminified source code, including any logic, API endpoint paths, or internal comments you'd rather keep private.

// BAD: next.config.ts — explicitly enabling source maps in production
const nextConfig: NextConfig = {
  productionBrowserSourceMaps: true, // your source code is now public
};
// GOOD: source maps off in production (Next.js default), on in dev
const nextConfig: NextConfig = {
  // productionBrowserSourceMaps defaults to false — don't change it
  // If you need error context in production, use Sentry's private source maps upload instead
};

If you need readable stack traces in production error reporting (you should), use Sentry's source map upload — it uploads maps to Sentry's servers, not your public CDN, so only your error monitoring tool can read them.

The dedicated entry on source maps exposed in production covers how attackers use them and what to check in your build output.


4. Verbose Error Messages

When your server throws an unhandled exception and returns a response like this:

Error: Cannot read properties of undefined (reading 'user')
  at /app/api/orders/route.ts:47:23
  at processTicksAndRejections (node:internal/process/task_queues:95:5)

...you've just handed the attacker your file system structure, the exact line that failed, and a hint about your data model. Real applications under active attack use error messages like this to map the application surface.

// BAD: returning raw errors to the client
export async function GET(req: Request) {
  try {
    const data = await fetchOrders();
    return Response.json(data);
  } catch (error) {
    // sends stack trace and internal path to the browser
    return Response.json({ error: (error as Error).message }, { status: 500 });
  }
}
// GOOD: generic message to client, full details to your logging system
import * as Sentry from "@sentry/nextjs";
 
export async function GET(req: Request) {
  try {
    const data = await fetchOrders();
    return Response.json(data);
  } catch (error) {
    // log the real error internally — Sentry, structured logs, whatever you use
    Sentry.captureException(error);
    // send nothing useful to the client
    return Response.json(
      { error: "Something went wrong. Please try again." },
      { status: 500 }
    );
  }
}

The rule: log details on the server, send generic messages to the client. No exceptions. Read more about verbose error messages as a vulnerability.


5. Default Credentials Left Active

This one sounds obvious, but it's responsible for real-world breaches at scale. Databases, admin panels, monitoring dashboards, CI systems, and message queues all ship with default usernames and passwords. If you install something and don't change the default credentials, anyone who knows the software's defaults can log in.

The most common examples:

  • MongoDB shipped with no authentication required until version 3.0
  • Redis exposes itself without authentication by default
  • Grafana defaults to admin / admin
  • Jenkins defaults to admin / password from /var/lib/jenkins/secrets/initialAdminPassword (sometimes left accessible)
  • phpMyAdmin exposed at /phpmyadmin with default MySQL credentials

The fix is simple: change defaults immediately after installation, before the service is reachable from any network. Use strong, unique passwords. Disable the default admin user entirely if the software allows creating separate admin accounts.


6. GraphQL Introspection in Production

GraphQL's introspection feature lets you query the API to discover its entire schema — every type, every query, every mutation, every field. It's indispensable in development. In production, it's a free reconnaissance tool for anyone looking to probe your API.

With introspection enabled, an attacker can:

  • Map every available query and mutation
  • Discover fields your UI doesn't expose but your API still accepts
  • Find deprecated fields that may lack proper authorization
  • Understand your data model well enough to craft targeted attacks
// BAD: Apollo Server with introspection enabled in production
import { ApolloServer } from "@apollo/server";
 
const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: true, // anyone can query your full schema
});
// GOOD: disable introspection based on environment
const server = new ApolloServer({
  typeDefs,
  resolvers,
  // introspection defaults to true in development, false in production
  // but be explicit:
  introspection: process.env.NODE_ENV !== "production",
});

The encyclopedia entry on GraphQL introspection enabled in production covers what an attacker can do with your schema and how to verify it's disabled.


7. Debug Routes in Production

Many frameworks and libraries add debug endpoints during development. Express has error-handling middleware that shows full stack traces. Rails has /rails/info/properties. Some custom-built apps have /debug, /health, or /__internal endpoints that were added for convenience and never removed before deploying.

These routes expose internal state, environment variables, dependency versions, and request logs. Check your routing configuration before every production deploy.

The entry on debug routes in production has a pattern checklist for common frameworks.


8. XXE — XML External Entity Injection

XXE happens when an application parses XML and the parser has external entity processing enabled. An XML document can define entities that reference external resources:

<!--  BAD: an attacker-controlled XML payload -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>&xxe;</data>

When your server parses this, &xxe; gets replaced with the contents of /etc/passwd. The attacker gets your server's user database in the parsed output.

The fix in most languages is a single configuration flag:

// Example using fast-xml-parser (Node.js)
import { XMLParser } from "fast-xml-parser";
 
// BAD: default config may allow entity expansion
const parser = new XMLParser();
 
// GOOD: explicitly prevent entity processing
const parser = new XMLParser({
  processEntities: false, // disables external entity processing
  allowBooleanAttributes: true,
});

OWASP merged XXE into A05:2021 because the root cause is always a misconfigured parser — one that ships with a dangerous default. Read the full explanation in XXE — XML External Entity injection.


9. Unnecessary Ports and Services Open

Every open port is an attack surface. If your server runs a database, a cache, an admin dashboard, and your application — and all of them are network-accessible — your attack surface is four times what it needs to be.

The minimal install principle: only run what you need, only expose it to the networks that need it, and close everything else.

For cloud deployments: use security groups and firewall rules to restrict database access to your application servers only. Redis and PostgreSQL should never be accessible from the public internet. Admin interfaces should live behind a VPN or bastion host, not on a public IP.


A Real-World Example: Microsoft Power Apps (2021)

In August 2021, Microsoft's Power Apps portal tables were misconfigured by default — they were publicly accessible without authentication. Researchers at UpGuard discovered that 38 million records across 47 entities (including American Airlines, Ford, and various state governments) were exposed because the default configuration for Power Apps portals set table permissions to allow public access.

This wasn't a zero-day exploit. It wasn't a sophisticated attack. It was a default setting that wasn't changed. The data included COVID-19 vaccination status, employee email addresses, and Social Security numbers.

Microsoft's response was to change the default and provide a scanning tool to identify misconfigured portals. But the damage — exposure of 38 million records — happened because of a default that shipped permissively.

This is what OWASP A05 means in practice. Not a sophisticated bypass, just a setting that should have been off and wasn't.


A Hardening Checklist for Next.js Apps

Run through this before every production deploy:

Headers

  • Content-Security-Policy configured (even a basic default-src 'self' is better than nothing)
  • X-Frame-Options: SAMEORIGIN — or use frame-ancestors in your CSP
  • X-Content-Type-Options: nosniff
  • Strict-Transport-Security with preload and at least 1-year max-age
  • Referrer-Policy: strict-origin-when-cross-origin
  • Check your headers free with the security header checker

CORS

  • No API route returns Access-Control-Allow-Origin: * for authenticated endpoints
  • CORS origin whitelist uses exact domain matching, not endsWith() or substring checks

Build

  • productionBrowserSourceMaps is not set to true in next.config.ts
  • Source maps are uploaded to Sentry (or equivalent) if you need production error context

Error handling

  • No API route returns raw error.message or error.stack to the client
  • Error responses use generic messages; details go to server-side logs

Services

  • GraphQL introspection disabled in production
  • No debug or internal routes accessible in production
  • No default credentials on any service your app depends on
  • Database and cache are not publicly accessible (security groups / firewall rules)

XML (if applicable)

  • XML parser configured to disable external entity processing

Prevention: Automate What You Can

The hardening checklist above is useful. But manual checklists fail at scale — things get missed, features get added, dependencies change. Automated configuration scanning catches misconfigurations continuously, not just at deploy time.

The security misconfiguration encyclopedia entry covers the full category with OWASP references and CWE mappings.

For Next.js specifically, Data Hogo scans your repository for all of the misconfigurations covered in this post: missing headers (detected in your next.config.ts), wildcard CORS in API routes, source maps enabled in production, verbose error handling, and GraphQL introspection. It also scans your deployed URL for live header configuration.

Scan your repo free → — the first 3 scans are free, no credit card required. Or check your live headers now with the free header checker.


Frequently Asked Questions

What is OWASP A05:2021 Security Misconfiguration?

OWASP A05:2021 Security Misconfiguration covers vulnerabilities that arise from incorrect or incomplete security settings — missing HTTP headers, overly permissive CORS, verbose error messages, default credentials, and more. It moved up from #6 to #5 in the 2021 OWASP Top 10 and now includes XXE (XML External Entity), which was its own separate category in 2017. According to OWASP's data, 90% of applications tested had some form of misconfiguration.

What are the most common security misconfigurations in Next.js?

The most common misconfigurations in Next.js apps are: missing security headers (CSP, X-Frame-Options, HSTS, X-Content-Type-Options), CORS set to wildcard (*) in API routes, source maps enabled in production builds, verbose error messages that expose stack traces to users, and GraphQL introspection left enabled. All of these are detectable with automated scanning.

How do I add security headers to a Next.js app?

Add a headers() function to your next.config.ts file. It returns an array of objects with source (the URL pattern) and headers (the headers to add). The minimum recommended headers are Content-Security-Policy, X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff, Strict-Transport-Security, Referrer-Policy, and Permissions-Policy. See the complete config example in this post.

Is CORS set to * always a vulnerability?

It depends on context. For a public API that intentionally serves data to any origin — like a public CDN or an open dataset — CORS wildcard is appropriate. For any API route that handles authenticated user data, account information, or mutations, wildcard CORS is a vulnerability. It allows any website to make cross-origin requests to your API on behalf of a logged-in user, which enables cross-site request forgery.

What is XXE and why is it now part of OWASP A05?

XXE (XML External Entity injection) happens when an application processes XML input that contains a reference to an external entity — a file path or URL that the XML parser will fetch and include in the output. An attacker can use this to read server-side files like /etc/passwd or trigger server-side request forgery. OWASP merged XXE into A05:2021 Security Misconfiguration because the root cause is always a misconfigured XML parser — one that has external entity processing enabled by default.

OWASPsecurity misconfigurationNext.jssecurity headersCORSCSPvibe-coding