Software Securityhome
Fix a findingSearch
Fix card · A07:2025 Authentication Failures

Fix missing authentication in Python and FastAPI

The vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.

In short

Put an authentication dependency on every protected path operation. Read the Authorization: Bearer token with HTTPBearer(auto_error=False), compare it to the server-side credential with hmac.compare_digest, and raise 401 with WWW-Authenticate: Bearer when it is missing or wrong. Keep the health probe open; a critical function like key rotation must never run unauthenticated.

Stack
  • Language Python
  • Runtime CPython 3.14.8
  • Framework FastAPI 0.142.2 (Starlette 1.7.0, Pydantic 2.13.5)
Maps to
Verified
Tests pass · fixed, vulnerable and wrong runs as expected
Other stacks9 cards
Download the project ZIP, 17 KB
On this page

Steps

  1. List the path operations that do something sensitive or change state. A critical function (here, rotating the signing key) must require authentication; a liveness probe can stay public.
  2. Add an authentication dependency with Depends(...) to each protected route (or dependencies=[...] on the decorator); read the credential with fastapi.security.HTTPBearer.
  3. Compare the presented token to the server-side credential with hmac.compare_digest (constant time).
  4. Raise HTTPException(401, headers={"WWW-Authenticate": "Bearer"}) when the credential is missing or does not match; the card uses HTTPBearer(auto_error=False) so one code path answers both cases.
  5. Ignore client-supplied hints about the caller (X-Internal, X-Forwarded-For, Host); the client writes them.
  6. Add regression tests that call each protected route with no credential and with a spoofed internal header, and assert 401.

Vulnerable, fixed and wrong

The three implementations share one API and one test suite; only the code below changes. The tests load the one that IMPL names.

The vulnerable code

Vulnerableimpl/vulnerable.pyPython
def add_routes(app: FastAPI) -> None:
    @app.get("/internal/metrics")
    def metrics():
        # No dependency: the caller is trusted because the route is "internal".
        return service.metrics()

    @app.post("/internal/rotate-key")
    def rotate_key():
        return {"keyId": service.rotate_key()}

    @app.get("/health")
    def health():
        return {"status": "ok"}

With IMPL=vulnerable, 6 of 9 tests fail (5 exploit tests).

The fix: require an authenticated operator

Fixedimpl/fixed.pyPython
def require_operator(creds: HTTPAuthorizationCredentials | None = Depends(_bearer)) -> None:
    if creds is None or not service.valid_operator_token(creds.credentials):
        raise HTTPException(
            status_code=401,
            detail="operator authentication required",
            headers={"WWW-Authenticate": "Bearer"},
        )


def add_routes(app: FastAPI) -> None:
    @app.get("/internal/metrics", dependencies=[Depends(require_operator)])
    def metrics():
        return service.metrics()

    @app.post("/internal/rotate-key", dependencies=[Depends(require_operator)])
    def rotate_key():
        return {"keyId": service.rotate_key()}

    @app.get("/health")
    def health():
        return {"status": "ok"}

With IMPL=fixed, 9 of 9 tests pass.

A fix that looks safe but is not

Wrong fiximpl/wrong.pyPython
def require_internal(request: Request) -> None:
    # A client-controlled header is not a credential: anyone can send it.
    if request.headers.get("X-Internal") != "1":
        raise HTTPException(status_code=401, detail="internal callers only")


def add_routes(app: FastAPI) -> None:
    @app.get("/internal/metrics", dependencies=[Depends(require_internal)])
    def metrics():
        return service.metrics()

    @app.post("/internal/rotate-key", dependencies=[Depends(require_internal)])
    def rotate_key():
        return {"keyId": service.rotate_key()}

    @app.get("/health")
    def health():
        return {"status": "ok"}

With IMPL=wrong, 5 of 9 tests fail (2 exploit tests).

The regression test

The exploit tests send the attack and check that it fails; functional tests next to them keep ordinary requests working, so a fix that rejects everything fails too.

Testtests/test_auth.pyPython
# Exploit: reach a critical function without being an authenticated operator.

def test_exploit_metrics_without_authentication_is_refused(client):
    assert client.get("/internal/metrics").status_code == 401


def test_exploit_rotate_key_without_authentication_is_refused(client):
    assert client.post("/internal/rotate-key").status_code == 401


def test_exploit_spoofed_internal_header_on_metrics_is_refused(client):
    assert client.get("/internal/metrics", headers={"X-Internal": "1"}).status_code == 401


def test_exploit_spoofed_internal_header_on_rotate_key_is_refused(client):
    assert client.post("/internal/rotate-key", headers={"X-Internal": "1"}).status_code == 401


def test_exploit_invalid_bearer_token_is_refused(client):
    res = client.get("/internal/metrics", headers={"Authorization": "Bearer not-the-token"})
    assert res.status_code == 401

Pitfalls

  • "It's internal" is not authentication: a reachable path operation needs a credential, because network position can be bypassed or reached from a compromised neighbour.
  • A client-supplied header such as X-Internal is set by the client, so a dependency that checks it authenticates nobody; only a value your own trusted proxy sets and strips can be relied on, and a real credential is better.
  • HTTPBearer only checks that an Authorization: Bearer header is present; it does not validate the token. With its default auto_error=True it answers a missing or malformed header with 401 and WWW-Authenticate: Bearer (checked on FastAPI 0.142.2; versions before 0.122.0 answered 403), but any non-empty bearer value passes it, so the dependency must still compare the token to your credential.
  • Comparing tokens with == leaks timing; use hmac.compare_digest.
  • A dependency declared but not attached to the route (forgetting dependencies=[...] or the Depends parameter) runs nothing; confirm the dependency actually guards the handler.
  • Returning 200 on a missing credential to avoid breaking clients defeats the control; fail closed with 401.
  • A single long-lived shared token is the simplest credential and is what this card uses to keep the tests self-contained; for calls between services, ASVS 5.0.0 13.2.1 asks for individual service accounts, short-lived tokens or certificates (mutual TLS) rather than unchanging shared keys, and the credential belongs in a secret store, not in source (Bandit's B105 flags the demo constant).

Verify the fix

  1. Run the tests in all three modes: IMPL=fixed must pass; IMPL=vulnerable and IMPL=wrong must fail (node ../../run-all.mjs missing-authentication/python-fastapi does this for you).
  2. Against your own endpoint, call each protected route with no Authorization header and with a spoofed X-Internal: 1 header; both must return 401. Call it with a valid Bearer token and confirm 200.
  3. No scanner checked here detects a route that lacks an authentication dependency; the regression tests are the control.

Scanner rules

What each rule reported when it was run on this card's code, or what its source shows it covers where it was not run. A rule that exists for the weakness does not always catch it.

Scanner rules for Fix missing authentication in Python and FastAPI
ToolRuleWhat it found here
Semgrepp/python, p/owasp-top-ten, p/security-audit, p/fastapiNo findings on any implementation. No rule can know which routes are meant to require authentication, so a route with no check, or one that trusts a client header, looks like any other route.Checked: Semgrep 1.180.0 (CE) in semgrep/semgrep:1.180.0@sha256:529ee8a277ec8adc5b534d7c74eea0a47e9de21d62852b6ba7ac6ba9566845c3; semgrep scan --metrics=off --config p/python --config p/owasp-top-ten --config p/security-audit --config p/fastapi --exclude tests .
BanditB105hardcoded_password_stringOne finding in every mode, LOW severity: OPERATOR_TOKEN in service.py, the demo credential the tests use. It is a fair flag -- in production the credential belongs in a secret store -- but it says nothing about whether the routes check it. No Bandit plugin detects missing authentication.Checked: Bandit 1.9.4 (pip install bandit==1.9.4 in a scratch venv, Python 3.14.8); bandit -r . -x ./.venv,./tests

Libraries and versions

Libraries and versions
ComponentVersion
LanguagePython
RuntimeCPython 3.14.8
FrameworkFastAPI 0.142.2 (Starlette 1.7.0, Pydantic 2.13.5)
fastapi0.142.2
starlette1.7.0
hmac, secrets (standard library)Python 3.14.8
httpx2 (TestClient transport, test only)2.13.1
pytest9.1.1

Run the tests

Download the project, install its dependencies once (this step needs the network), then run the tests against each implementation from the missing-authentication/python-fastapifolder.

Shell
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
Shell
# Expected: every test passes
IMPL=fixed .venv/bin/python -m pytest

# Expected: an exploit test fails
IMPL=vulnerable .venv/bin/python -m pytest

# Expected: a test fails
IMPL=wrong .venv/bin/python -m pytest

Each run writes a JUnit report to reports/junit.xml. From the top folder of the download, node run-all.mjs missing-authentication/python-fastapi runs all three modes and checks the expected results.

Download the project ZIP, 17 KB

Verified

Machine
macOS 27.0 (Darwin 27.0.0, arm64)
Toolchain
  • python 3.14.8
  • pip 26.2.1
  • fastapi 0.142.2
  • starlette 1.7.0
  • pydantic 2.13.5
  • httpx2 2.13.1
  • pytest 9.1.1
Matrix
fixed: 9 of 9 tests pass; vulnerable: 6 of 9 tests fail (5 exploit tests); wrong: 5 of 9 tests fail (2 exploit tests)
Test results
IMPL=fixed 9/9 pass; IMPL=vulnerable fails 6/9 (5 exploit: both no-auth calls, both spoofed-header calls, the invalid token; plus the missing-token functional assertion); IMPL=wrong fails 5/9 (2 exploit: the two spoofed-header calls; plus the valid-token and missing-token functional assertions).
Dependency freshness
fastapi 0.142.2 (2026-09-30), starlette 1.7.0 (2026-09-23), pydantic 2.13.5 (2026-08-28) are pinned; fastapi 0.143.0 (2026-10-08) changed OpenTelemetry defaults and pydantic 2.14.x were under a week old, so the card holds the prior line, matching the other FastAPI cards. Python 3.14.8 is the 30 September 2026 security release.
Scanners
Semgrep 1.180.0 in the pinned semgrep/semgrep image and Bandit 1.9.4 from a scratch venv, run (see scannerRules); no CodeQL query covers this weakness.

Versions and results hold for the toolchain above. The card's tests are the check: run them with your own versions before relying on the fix.

References

  1. CWE-306: Missing Authentication for Critical Function
  2. FastAPI: Security (HTTPBearer, dependencies)
  3. OWASP Authentication Cheat Sheet
  4. OWASP ASVS 5.0.0 V13 Configuration (13.2.1 authenticated backend communication)
  5. Python hmac.compare_digest (constant-time comparison)

Attack patterns:

Find your next answer

Type to search. Search runs in your browser.

Searches run in your browser, but the host can infer them from the index files they load. The address shows your search so you can bookmark or share it.