Flask and FastAPI sit at opposite ends of Python's web framework spectrum - Flask minimal and unopinionated, FastAPI built around type hints and automatic validation - but a real security incident in either usually traces back to one of a small, shared set of gaps. This checklist covers both, noting where they differ.

1. Never Run debug=True in Production (Flask)

# Vulnerable in production
app.run(debug=True)

# Fixed: debug mode driven by environment, off by default
app.run(debug=os.environ.get('FLASK_DEBUG', '0') == '1')

This is more severe than a typical debug-mode exposure elsewhere: Flask's debug mode activates the Werkzeug interactive debugger, which lets anyone who reaches an unhandled exception execute arbitrary Python code directly through the browser. The PIN that's supposed to protect the debugger console is derived from machine-specific values (MAC address, machine id, parts of the app's own source path) that have been shown to be predictable or independently discoverable in some deployment environments - treat a debug-mode-enabled production Flask app as full remote code execution, not just an information leak.

2. Restrict OpenAPI Docs in Production (FastAPI)

# Disable interactive docs in production, or gate them behind auth
app = FastAPI(
    docs_url="/docs" if settings.ENV != "production" else None,
    redoc_url=None,
)

FastAPI's automatic /docs and /redoc are genuinely useful during development and expose your complete API surface - every route, parameter, and schema - to anyone who requests the path if left enabled publicly.

3. SQL Injection via Raw Queries

# Vulnerable: string-formatted SQL, in either framework
cursor.execute(f"SELECT * FROM orders WHERE status = '{status}'")
session.execute(text(f"SELECT * FROM orders WHERE status = '{status}'"))

# Fixed: parameterized, whether using DB-API directly or SQLAlchemy's text()
cursor.execute("SELECT * FROM orders WHERE status = %s", (status,))
session.execute(text("SELECT * FROM orders WHERE status = :status"), {"status": status})

4. Secrets: python-dotenv Loads Values, Doesn't Protect Them

python-dotenv reads a .env file into os.environ at startup - it provides zero protection on its own, and a .env file committed to version control or served as a static file is exactly as exposed as a Laravel .env file. Keep it gitignored and out of any directory a web server might serve directly.

5. CORS

# FastAPI
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://yourapp.com"],
    allow_credentials=True,
)

# Flask
from flask_cors import CORS
CORS(app, origins=["https://yourapp.com"], supports_credentials=True)

The wildcard-origin-plus-credentials combination that's dangerous in every framework is dangerous here too - never pair allow_origins=["*"] with allow_credentials=True.

6. Session and Cookie Security

# Flask
app.config.update(
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE='Strict',
)

7. Input Validation: Pydantic vs Manual

FastAPI's Pydantic models validate types and constraints automatically at the request-parsing layer, which closes an entire class of type-confusion bugs by default - Flask has no equivalent built in, so request data validation (checking that a field is actually the type and range expected, not just present) has to be added deliberately with a library like marshmallow or hand-written checks, and is easy to skip under time pressure.

8. Rate Limiting

# Flask
from flask_limiter import Limiter
limiter = Limiter(app, default_limits=["200 per day"])

@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute")
def login(): ...

# FastAPI
from slowapi import Limiter
limiter = Limiter(key_func=get_remote_address)

@app.post('/login')
@limiter.limit("5/minute")
async def login(request: Request): ...

9. Dependency Scanning

pip-audit

Run in CI - Python's package ecosystem has had real supply-chain incidents (typosquatted packages, compromised maintainer accounts), and pinned versions in a requirements file don't announce on their own when a CVE is disclosed against them.

10. Security Headers

# Flask - flask-talisman sets a reasonable default set
from flask_talisman import Talisman
Talisman(app, content_security_policy={'default-src': "'self'"})

# FastAPI has no direct equivalent - set headers via middleware
@app.middleware("http")
async def add_security_headers(request, call_next):
    response = await call_next(request)
    response.headers["X-Content-Type-Options"] = "nosniff"
    return response

Verifying From the Outside

Try Shieldome's Python security scanner to check for exposed debug consoles, unrestricted API docs, and missing headers from the outside. Create a free account to run your first scan.