Security Labs · Lab 3

Security Misconfiguration

The code can be perfect and the system still wide open — because the danger is in the configuration. A CORS line that says yes to everyone, an error that prints the database password, a secret committed to a repo. This lab teaches you to read config for those holes, fix them, and test for them.

Security Labs OWASP A05: Security Misconfiguration — Lab 3 of 3 ~30 min read · ~70 min with exercises

1 The Hook

Pae Pay, a fictional NZ payments startup, shipped a clean, well-tested API. The code reviews were thorough, the unit tests were green, and there was not an injection flaw or a missing access check anywhere in it. It still leaked customer data on day one.

The problem was one line in a configuration file. To make front-end development easier, someone had set the API’s cross-origin policy to allow requests from any website — a wildcard — and to send credentials along with them. That meant a malicious site, opened in a logged-in customer’s browser, could quietly call the Pae Pay API as that customer and read the response. No code was broken. The configuration simply told the browser it was fine to share customer data with anyone who asked.

A tester found it not by reading application code but by reading the response headers and the config. The header Access-Control-Allow-Origin: * sitting next to Access-Control-Allow-Credentials: true is a known-dangerous combination, and it was right there in plain sight.

This is security misconfiguration — OWASP A05. The system is insecure not because of a coding bug but because of how it was set up: an over-permissive setting, a missing protective header, an error message that says too much, a secret left where it can be read. These flaws are everywhere because configuration is easy to get wrong and easy to forget to check.

The lesson: security is not only in the code. You have to read the configuration, the headers, and the error responses with the same suspicion you bring to a query string.

2 The Rule

A system is only as secure as its configuration, not just its code. Default settings, wildcards, verbose error messages, and exposed secrets are vulnerabilities even when every line of application code is correct. If a setting grants more access than the system needs, or an error or header reveals more than a stranger should see, you have found a misconfiguration — test the config and the responses, not only the code.

3 The Analogy

Analogy

A brand-new house with great locks — and the installer’s default code still on the alarm, and a key under the mat.

You can build a house in Christchurch with the strongest doors and the best alarm on the market — that is the well-written code. But if the alarm still has the installer’s default 0000 code, the back window is propped open for the painters, and there is a spare key under the mat, the quality of the locks is irrelevant. Anyone who knows the defaults walks straight in. That is security misconfiguration: the building is sound, the setup is not.

Securing it is not rebuilding the house — it is changing the default code, shutting the window, and taking the key inside. In a system, that is removing wildcards, adding the protective headers, silencing verbose errors, and moving secrets out of the code.

4 What Security Misconfiguration Is

Misconfiguration is the gap between “the code is correct” and “the system is secure.” It covers every way a system can be set up to give away more than it should. OWASP groups it under A05: Security Misconfiguration, and the common forms are easy to recognise once you know them:

  • Over-permissive CORS — a cross-origin policy that allows any website to call the API, especially with credentials.
  • Missing security headers — no Strict-Transport-Security, no Content-Security-Policy, no X-Content-Type-Options, leaving browsers unprotected.
  • Verbose error messages — stack traces, SQL, file paths, or version numbers returned to the client.
  • Exposed secrets — API keys, passwords, or tokens hard-coded in source, committed to a repo, or printed in logs.
  • Defaults left on — default admin accounts, debug mode in production, sample pages, open management ports.

None of these is a coding bug in the usual sense. They are decisions about how the system is run, and they are a tester’s job to catch because they are invisible to functional testing — the feature works perfectly while the door stands open.

Pro tip: The fastest misconfiguration check is to read the response headers of any request and ask two questions: “does this header give away anything about the server or the data?” and “is a protective header that should be here missing?” You can do that with browser dev tools in under a minute.

5 Spotting Bad CORS

CORS — Cross-Origin Resource Sharing — controls which other websites may read responses from your API. Get it wrong and any site can read your users’ data. Here is the Pae Pay config, written so the dangerous combination is visible:

# API CORS config — VULNERABLE
cors:
  allow_origin: "*"        # any website
  allow_credentials: true  # send cookies/auth with the request
  allow_methods: ["GET","POST","PUT","DELETE"]

The fatal combination is allow_origin: "*" together with allow_credentials: true. The wildcard says “any website may call this API” and the credentials flag says “and the browser may send the logged-in user’s cookies with the call.” So a malicious page that a logged-in customer visits can call the API as that customer and read the response — their balance, their details, anything the API returns.

The tell-tale signs you read for:

  • A wildcard origin (*) on an API that returns private data.
  • Credentials allowed alongside a broad origin — the combination is what makes it exploitable.
  • Origin reflected from the request — a config that echoes back whatever Origin the caller sent is effectively a wildcard.

6 Missing Headers, Verbose Errors, and Exposed Secrets

CORS is one misconfiguration; here are the other three you read for most often.

Missing security headers

Protective response headers tell the browser how to defend the user. Their absence is a misconfiguration. The key ones: Strict-Transport-Security (force HTTPS), Content-Security-Policy (limit what scripts can run), X-Content-Type-Options: nosniff, and a sensible X-Frame-Options or frame policy to prevent clickjacking. Read the headers; if these are absent on a page handling personal data, that is a finding.

Verbose error messages

An error that returns a full stack trace, a SQL statement, a file path, or a framework version hands an attacker a map of the system. A fictional Te Whatu Ora booking API that responds to a bad request with the database connection string and the server version has told a stranger exactly how to attack it. Production errors should be generic to the client and detailed only in the server logs.

Exposed secrets

An API key, database password, or token hard-coded in source, committed to a repository, or printed into logs is a secret that anyone with access to the code or the logs can read. For NZ systems handling personal information, an exposed credential is a direct path to a Privacy Act 2020 breach. Secrets belong in a secrets manager or environment configuration, never in the source.

Pro tip: The NZISM expects hardening — defaults removed, unnecessary services off, secrets managed, errors handled safely. A misconfiguration finding maps directly to a hardening control, so framing your report against the NZISM gives it weight with the people who fix it.

7 The Fix: Lock It Down

The fix for misconfiguration is to grant only what is needed and reveal only what is necessary. Here is the Pae Pay CORS config, fixed:

# API CORS config — FIXED
cors:
  allow_origin: ["https://app.paepay.co.nz"]  # our app only
  allow_credentials: true
  allow_methods: ["GET","POST"]  # only what is used
# plus security headers on every response:
# Strict-Transport-Security, Content-Security-Policy,
# X-Content-Type-Options: nosniff, X-Frame-Options: DENY

The wildcard is gone — the origin is now an explicit allow-list of the one site that should call this API. Methods are trimmed to those actually used. And the protective headers are added on every response. A malicious third-party site can no longer read the API on a customer’s behalf.

The rules that make a fix correct:

  • Allow-list, never wildcard — name the exact origins; do not reflect the request’s origin back.
  • Add the protective headers on every response, especially for pages handling personal data.
  • Generic errors to the client, detail to the logs — never return stack traces, SQL, or versions to the caller.
  • Secrets out of the code — into a secrets manager or environment configuration, and turn debug mode off in production.

8 Building Misconfiguration Test Cases

Misconfiguration testing reads responses and config rather than driving the UI. The test case names what is sent, what header or message comes back, and what must not appear.

Test ID: SEC-CFG-002
Vulnerability: Over-permissive CORS (OWASP A05)
Endpoint: GET /api/account — any authenticated endpoint
Steps: Send the request with header Origin: https://evil.example
Expected result: Response does NOT include Access-Control-Allow-Origin: *
                  and does NOT reflect evil.example back as an allowed origin.
Must NOT happen: Allow-Origin is * or echoes the attacker origin while
                  Allow-Credentials is true.
Negative control: Origin: https://app.paepay.co.nz — allowed normally.
Evidence: Captured response headers for both origins.
Traceability: Risk R-09 (cross-origin data exposure).
Result: [Pass / Fail]

A full misconfiguration suite also checks: the protective headers are present on responses (HSTS, CSP, nosniff, frame policy); a deliberately malformed request returns a generic error with no stack trace, SQL, path, or version; and no secret appears in any response, source bundle, or log. The negative control — the legitimate origin still being allowed — matters here too, so the lock-down does not break the real front end.

9 Common Mistakes

🚫 Assuming clean code means a secure system

Why it happens: Code review and tests pass, so the system feels safe.
The fix: Misconfiguration is insecurity in the setup, not the code — a wildcard CORS line, a missing header, an exposed secret. Read the config and the response headers as carefully as you read the code, because functional testing will never surface them.

🚫 Leaving a development CORS wildcard in production

Why it happens: allow_origin: "*" makes local front-end development painless, and it never gets tightened.
The fix: A wildcard origin, especially with credentials allowed, lets any site read your users’ data. Replace it with an explicit allow-list of your own origins before go-live, and test that an attacker origin is rejected.

🚫 Showing detailed errors to the user “to help debugging”

Why it happens: Stack traces and SQL in the response speed up development.
The fix: Verbose errors hand an attacker a map of the system — framework, versions, file paths, even the database. Return a generic message to the client and keep the detail in server logs. Turn debug mode off in production.

🚫 Treating a hard-coded secret as “temporary”

Why it happens: Pasting a key into the code to get something working feels quick and reversible.
The fix: Secrets in source get committed, copied, and logged, and a committed secret is compromised the moment it lands in history. Use a secrets manager or environment configuration, and scan the repo and logs as part of testing.

10 Now You Try

Three graded exercises: spot the misconfiguration, fix the config, then design the tests. Write your answer, run it for AI feedback, then compare to the model answer. Every snippet here is a safe teaching example — you are identifying and repairing flaws, never attacking a real system.

🔍 Exercise 1 of 3 — Spot the Vulnerability

Read the configuration snippet below from a fictional NZ payments API. Identify the misconfigurations (there is more than one), name each, and explain how an attacker would abuse them.

# payments-api config
debug: true
cors:
  allow_origin: "*"
  allow_credentials: true
database:
  url: "postgres://admin:Summer2024!@db.internal:5432/payments"
error_handler:
  return_stack_trace_to_client: true

List the misconfigurations and how each is abused:

Show model answer
All four lines are OWASP A05 security misconfigurations:

1. debug: true in production — exposes detailed internals, often interactive debuggers or verbose pages. An attacker learns framework, versions, and sometimes can execute code. Turn debug off in production.

2. CORS allow_origin "*" WITH allow_credentials true — the dangerous combination. A malicious site a logged-in customer visits can call this API as that customer and read the response (their payment data). Fix: explicit origin allow-list, never a wildcard with credentials.

3. Hard-coded database credentials in the config — url contains admin:Summer2024! in plain text. Anyone who can read the config or repo history has full database access, including all payment records — a direct Privacy Act 2020 breach. Fix: move secrets to a secrets manager / environment, rotate the leaked password.

4. return_stack_trace_to_client: true — verbose errors hand the attacker a map: file paths, SQL, versions, sometimes the connection string. Fix: generic errors to the client, detail only in server logs.

Strong answers name the dangerous CORS COMBINATION specifically (not just "wildcard") and flag the leaked secret as the most severe. This is a safe defensive exercise.
🔧 Exercise 2 of 3 — Fix the Config

Rewrite the config below so the misconfigurations are closed. It is a fictional RealMe-integrated government services API. Use an explicit CORS allow-list, turn off debug and verbose errors, move the secret out of the file, and note the security headers you would add.

Vulnerable original:
debug: true
cors: { allow_origin: "*", allow_credentials: true }
api_key: "sk_live_8f2a91councilservices"
errors: { verbose: true }

Write the hardened config:

Show model answer
# hardened config
debug: false
cors:
  allow_origin: ["https://services.govt.nz"]   # explicit allow-list, no wildcard
  allow_credentials: true
  allow_methods: ["GET", "POST"]
api_key: ${API_KEY}        # injected from a secrets manager / environment, NOT in the file
errors:
  verbose: false           # generic message to client; full detail to server logs only
# security headers to add on every response:
#   Strict-Transport-Security: max-age=31536000; includeSubDomains
#   Content-Security-Policy: default-src 'self'
#   X-Content-Type-Options: nosniff
#   X-Frame-Options: DENY   (or frame-ancestors 'none' in CSP)

What makes this correct: the wildcard origin is replaced with the one legitimate origin; debug and verbose errors are off; the secret is referenced from the environment/secrets manager rather than written in the file (and the leaked key should be rotated); and the protective headers are added. A fix that keeps the wildcard, leaves the key in the file, or only changes one of the four is not complete.
🏗️ Exercise 3 of 3 — Design the Test Cases

Design 4 security test cases to prove a fictional ANZ-style banking API is free of common misconfigurations. Cover CORS, security headers, verbose errors, and exposed secrets. Each case needs: an ID, the steps, the expected result, and what must NOT happen. Include a negative-control case.

Show model answer
SEC-CFG-01 (CORS) | Steps: send a request with Origin: https://evil.example to an authenticated endpoint | Expected: the response does not return Access-Control-Allow-Origin: * and does not reflect evil.example; the attacker origin is not allowed | Must NOT happen: wildcard or reflected origin combined with Allow-Credentials: true

SEC-CFG-02 (headers) | Steps: inspect the response headers of a page/endpoint handling account data | Expected: Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options: nosniff, and a frame policy are all present | Must NOT happen: any of these protective headers is missing on a sensitive response

SEC-CFG-03 (verbose errors) | Steps: send a deliberately malformed request (bad JSON, wrong type) | Expected: a generic error message and a safe status code | Must NOT happen: a stack trace, SQL, file path, framework version, or connection string is returned to the client

SEC-CFG-04 (secrets + negative control) | Steps: search the deployed source bundle, config responses, and logs for keys/passwords; AND send a request from the legitimate origin https://app.anz... | Expected: no secret is found anywhere reachable; the legitimate origin IS allowed and the API works normally | Must NOT happen: a hard-coded API key/password is exposed; OR the hardening blocks the real front end

Bonus: capture headers/responses as evidence and trace each to a numbered risk. Weak suites test only CORS, or omit the negative control so a too-strict config "passes" while breaking the real app — that omission is the difference being marked.

11 Self-Check

Click each question to reveal the answer.

Q1: In one sentence, what is security misconfiguration?

Insecurity that comes from how a system is set up rather than from a coding bug — over-permissive settings, missing protective headers, verbose error messages, exposed secrets, or defaults left on — so the code can be correct while the system is still wide open. It is OWASP A05.

Q2: Why is allow_origin: "*" with allow_credentials: true dangerous?

The wildcard lets any website call the API, and the credentials flag lets the browser send the logged-in user’s cookies with that call. So a malicious site a logged-in user visits can call the API as that user and read the response — their private data. The combination is the exploit; replace the wildcard with an explicit allow-list.

Q3: Why are verbose error messages a security problem?

A stack trace, SQL statement, file path, or version number returned to the client hands an attacker a map of the system — what it is built on, where things are, and how to attack it. Production errors should be generic to the client, with the detail kept only in server logs.

Q4: Where should secrets live, and why not in the source?

In a secrets manager or environment configuration, never in the source. A secret hard-coded in code gets committed to history, copied, and printed to logs, and a committed secret is compromised the moment it lands — for NZ systems that is a direct path to a Privacy Act 2020 breach. A leaked secret must also be rotated.

Q5: Why does a misconfiguration test suite still need a negative control?

Because a too-strict lock-down can break the real application — for example, removing the wildcard but failing to allow the legitimate front-end origin. The negative control confirms the genuine origin is still allowed and the API still works, proving the system is secure and functional.

12 Interview Prep

Real questions asked in NZ QA interviews for security-aware testing roles. Read the model answers, then practise your own version.

“The code passed review and all tests are green. Why would you still test for security?”

Because a large class of vulnerabilities lives in the configuration, not the code — OWASP calls it A05, security misconfiguration. A system with perfect code can still leak data through a wildcard CORS policy, a missing security header, a verbose error that prints the database connection string, or a secret committed to the repo. Functional tests never surface those because the feature works fine while the door stands open. So I read the response headers, the CORS settings, the error responses, and scan for exposed secrets, with the same suspicion I bring to a query string. Clean code and a secure system are two different claims.

“How would you test an API’s CORS configuration?”

I’d send a request with an Origin header set to a site that should not be trusted, like https://evil.example, against an authenticated endpoint, and inspect the response. The dangerous outcome is Access-Control-Allow-Origin: *, or the server reflecting my attacker origin back, while Access-Control-Allow-Credentials is true — that combination lets a malicious site read a logged-in user’s data. I’d confirm the attacker origin is rejected, and then run a negative control with the legitimate front-end origin to make sure it is still allowed and the app works. I’d capture the headers for both as evidence.

“You find an API key hard-coded in the repo. What do you do?”

I treat it as a live incident, not just a code-quality note. A committed secret is compromised the moment it lands in history, so first I’d raise it so the key is rotated — removing it from the latest file is not enough because it is still in the history. Then I’d confirm where it should have lived: a secrets manager or environment configuration, injected at runtime. I’d also check the logs and any deployed bundle for the same secret, because keys leak in more than one place. For an NZ system handling personal information, an exposed credential is a direct path to a Privacy Act 2020 breach, so I’d write it up against the NZISM hardening expectation and trace it to a risk.