Skip to content
back to the map
practice
stage: ship
2 min read

A02 Security Misconfiguration

Defaults are built for convenience, not for the internet. A debug flag, an open bucket, a verbose error or a default password turns correct code into an incident.

TrivyCheckovScoutSuite / ProwlerMozilla ObservatorynmapOpenSCAP / CIS benchmarks

What it is

is any gap between how a system is set up and how it should be: unnecessary features enabled, default accounts left in place, debug mode in production, missing security headers, permissive cloud permissions, or stack traces shown to users.

It is A02 in :2025 because it is everywhere and cheap to find: attackers scan the whole internet for the same few mistakes.

How the attack works

  • A framework left in debug mode exposes an interactive console or full stack traces with file paths and secrets.
  • A storage bucket, database or admin panel is reachable from the internet with default or no credentials.
  • Directory listing or a leftover /.git, /.env or backup file leaks source and keys.
  • XML parsers with external entities enabled let a crafted document read local files.
  • Permissive (a wildcard origin with credentials) lets any website call the API as the victim.

Vulnerable configuration

Three common shapes: debug on, wildcard CORS with credentials, and a public container exposing a database.

app.py (Flask)python
app = Flask(__name__)
app.config["DEBUG"] = True            # interactive debugger reachable in production
CORS(app, origins="*", supports_credentials=True)
docker-compose.ymlyaml
services:
  db:
    image: postgres:latest
    ports:
      - "5432:5432"            # published on every interface
    environment:
      POSTGRES_PASSWORD: postgres
bucket.tfhcl
resource "aws_s3_bucket_public_access_block" "assets" {
  bucket                  = aws_s3_bucket.assets.id
  block_public_acls       = false
  block_public_policy     = false
}

The fix

Make the secure setting the default and build it once: a hardened base image, a reviewed configuration module, and the same settings in every environment, applied from code. Remove what you do not use.

app.pypython
app = Flask(__name__)
app.config["DEBUG"] = os.environ.get("APP_ENV") == "development"
CORS(app, origins=["https://app.example.com"], supports_credentials=True)

@app.after_request
def set_headers(resp):
    resp.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
    resp.headers["X-Content-Type-Options"] = "nosniff"
    resp.headers["Content-Security-Policy"] = "default-src 'self'"
    return resp
docker-compose.ymlyaml
services:
  db:
    image: postgres:16
    expose:
      - "5432"                 # reachable only by other services on the network
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
bucket.tfhcl
resource "aws_s3_bucket_public_access_block" "assets" {
  bucket                  = aws_s3_bucket.assets.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Prove the fix

Misconfiguration is verified by scanning, not by reading code. Run a config scanner in CI against and images, and run a small check against the deployed app. The test fails the build when a safe default regresses.

ci-checks.shbash
# 1. infrastructure code: fail on misconfigured resources
checkov -d infra/ --quiet --compact

# 2. container image: fail on HIGH/CRITICAL findings
trivy image --exit-code 1 --severity HIGH,CRITICAL registry.example.com/app:$GIT_SHA

# 3. deployed app: required headers must be present
curl -sI https://staging.example.com | grep -qi '^strict-transport-security:' || exit 1
curl -sI https://staging.example.com | grep -qi '^content-security-policy:' || exit 1
test_config.py (pytest)python
def test_debug_is_off_in_production(monkeypatch):
    monkeypatch.setenv("APP_ENV", "production")
    assert create_app().config["DEBUG"] is False

def test_cors_does_not_allow_any_origin(client):
    resp = client.options("/api/me", headers={"Origin": "https://evil.example"})
    assert resp.headers.get("Access-Control-Allow-Origin") != "*"

Checklist

  • One repeatable, automated hardening process for every environment; no hand-tuned servers.
  • Remove unused features, sample apps, default accounts, and open ports.
  • Send the security headers that apply (HSTS, CSP, X-Content-Type-Options, frame protection).
  • Show generic errors to users; send details to logs, not to the response.
  • Review cloud permissions and storage exposure continuously, not once.
  • Treat configuration as code: reviewed, versioned, scanned.
Security misconfigurationCorrect software running with unsafe settings: debug on, default credentials, public storage, permissive CORS, missing headers.see in glossary →OWASP Top 10The best-known reference list of web application security risks (A01 broken access control, A02 cryptographic failures, A03 injection...), revised every few years by the OWASP community. It is an educational map of risk categories, not a tool checklist or a certification.see in glossary →CORScross-origin resource sharingA browser mechanism that lets a server say which other origins may read its responses. A wildcard origin combined with credentials is a misconfiguration.see in glossary →IaCinfrastructure as codeDescribing infrastructure in versioned files (Terraform, Ansible). It brings review, history and testing to configuration, but replicates mistakes at scale if it is not scanned.see in glossary →

connections on the map

updated on 14/10/2026 · sources to go deeper · report an error

in other domains

test what you learned

Loading questions…