v0.14 is out: a mobile auth pack for Swift/iOS and Android, catching insecure token storage, cleartext traffic, and OAuth in embedded WebViews. Read more →
HIGH AI PREVALENCE: MEDIUM auth.py.fastapi.hardcoded-api-key

A FastAPI security dependency (Security(...), an API-key scheme such as APIKeyHeader/APIKeyQuery/APIKeyCookie, or an OAuth2 bearer scheme) injects a credential that is then compared against a hard-coded string literal.

Why AI tools produce this: AI coding tools produce this regularly, typically when prompted for a shortcut or a quick fix.

Why this matters

The accepted key/token is baked into the source, so anyone who reads the code (or a leaked repo) can authenticate, and the secret cannot be rotated without a redeploy (CWE-798). This is a common AI-generated shortcut: a single inline literal replaces a real key store.

Compare against a secret loaded from the environment or a secret manager with a constant-time check instead, e.g. secrets.compare_digest(api_key, os.environ["API_KEY"]), and issue/verify per-client keys rather than one shared literal.

VULNERABLE
vulnerable.py
import secrets
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, Security, status
from fastapi.security import APIKeyHeader

app = FastAPI()
api_key_header = APIKeyHeader(name="X-API-Key")


def get_api_key(api_key: str = Security(api_key_header)):
    # ruleid: auth.py.fastapi.hardcoded-api-key
    if api_key == "my-super-secret-api-key":
        return api_key
    raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)


def get_api_key_annotated(
    api_key: Annotated[str, Security(api_key_header)],
):
    # ruleid: auth.py.fastapi.hardcoded-api-key
    if not secrets.compare_digest(api_key, "another-hardcoded-key"):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
    return api_key


async def get_api_key_async(api_key: str = Security(api_key_header)):
    # ruleid: auth.py.fastapi.hardcoded-api-key
    if secrets.compare_digest(api_key.encode("utf8"), b"literal-bytes-key"):
        return api_key
    raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
SAFE
safe.py
import os
import secrets
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, Security, status
from fastapi.security import APIKeyHeader

app = FastAPI()
api_key_header = APIKeyHeader(name="X-API-Key")


def get_api_key(api_key: str = Security(api_key_header)):
    # Compared against a secret from the environment.
    # ok: auth.py.fastapi.hardcoded-api-key
    if secrets.compare_digest(api_key, os.environ["API_KEY"]):
        return api_key
    raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)


def get_api_key_lookup(
    api_key: Annotated[str, Security(api_key_header)],
):
    # Verified against a key store — no literal.
    # ok: auth.py.fastapi.hardcoded-api-key
    if is_valid_api_key(api_key):
        return api_key
    raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)


def get_role(role: str = Depends(current_role)):
    # Generic Depends value compared to a literal — an authorization check,
    # NOT a credential. Must not fire (why the anchor is Security, not Depends).
    # ok: auth.py.fastapi.hardcoded-api-key
    if role == "admin":
        return role
    raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)

Suppressing this rule

If a finding is a genuine false positive, scope the suppression to the exact line and leave a reason, never disable the rule project-wide. Disable directives are line-scoped by design.

# oauthlint-disable-next-line auth.py.fastapi.hardcoded-api-key -- <reason>

References

https://fastapi.tiangolo.com/reference/security/ ↗https://cwe.mitre.org/data/definitions/798.html ↗