KAHIBARO
Discord Login Register

Security Review

Why a Security Review Matters

When you reach the end of a project, it is tempting to ship as soon as things “work.” For a production backend, that is not enough. You must also ask, “Is this safe to put on the internet?”

A security review is a focused, systematic check of your application, its configuration, and its infrastructure, from a security perspective. In this chapter you will:

You will not become a professional security auditor from one chapter, but you will gain a repeatable process that significantly reduces obvious and common risks.

Goal of a security review:
Before going to production, you must actively try to find ways your system can be abused, attacked, or misconfigured, and fix or mitigate them, instead of assuming it is safe because “tests pass.”

A Practical Security Review Framework

You can structure your security review into several layers:

  1. Code and dependencies
  2. Authentication and authorization
  3. Data validation and input handling
  4. Secrets and configuration
  5. Transport security and network configuration
  6. Database and storage
  7. Operational security (logging, monitoring, backups, recovery)

Think of these as a checklist you can go through, line by line, for your final project.

A simple way to track your review is a table like this:

AreaCheckStatusNotes / Fix
AuthPasswords hashed with strong algorithmDoneUsing Argon2
SecretsNo secrets in GitNeeds fixRemove .env from repo
HTTPAll traffic forced to HTTPSPendingConfigure redirect in Nginx
DBApp user has least privilegesDoneNo SUPERUSER

You can keep this as a markdown file in the repo, for example SECURITY_REVIEW.md.

1. Code and Dependency Security

Even if your own code is careful, your dependencies can introduce security risks. You also want to avoid easy coding mistakes.

Remove Insecure Debug Settings

Look for anything you enabled during development that should be disabled in production.

Examples:

In a FastAPI + Uvicorn setup, you might have:

bash
uvicorn app.main:app --reload

In production, remove the --reload flag, and run behind Gunicorn or another process manager.

If you used custom config:

python
# config.py
DEBUG: bool = True  # Development

Ensure the production config has:

python
DEBUG: bool = False

And in your app, avoid exposing debug data:

python
if settings.DEBUG:
    # Maybe log extra details
    ...

Avoid Leaking Sensitive Data in Logs and Errors

Search your code for print, logger.debug, and exception handlers.

Bad examples:

python
logger.info(f"Login attempt for {email} with password {password}")
python
except Exception as e:
    return JSONResponse(
        status_code=500,
        content={"error": str(e)},  # Might leak SQL, file paths, etc.
    )

Better:

python
logger.info("Login attempt for %s", email)  # No password
except Exception:
    logger.exception("Unhandled server error")
    return JSONResponse(
        status_code=500,
        content={"error": "Internal server error"},
    )

Never log or return in responses:

  • Passwords or password hashes.
  • Access tokens, refresh tokens, API keys.
  • Full SQL queries with parameter values.
  • Full stack traces in production responses.

Dependency Vulnerabilities

Use tools to scan for known vulnerabilities in Python packages and system dependencies.

Typical tools:

bash
  pip install pip-audit
  pip-audit

Review any reported vulnerabilities and upgrade or replace packages where needed. For example, if pip-audit reports an issue in an old requests version, update:

bash
pip install "requests>=2.32.0"

Lock dependencies with a requirements.txt or poetry.lock and avoid arbitrary version drift.

2. Authentication and Authorization Review

For the final project, you already implemented auth. Now you must verify it is robust and correctly enforced.

Password Storage and Login Flow

Check your registration and login code.

Bad example:

python
# Never do this
user.password = password  # Plain text
def verify_password(plain, stored):
    return plain == stored

Correct style:

python
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
def hash_password(password: str) -> str:
    return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)

Confirm:

JWT and Session Security

If you use JWT for auth, review:

Example in FastAPI:

python
from datetime import datetime, timedelta
import jwt
def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(
        to_encode, settings.JWT_SECRET_KEY, algorithm="HS256"
    )
    return encoded_jwt

Verify that:

Enforce Authorization Everywhere

Find all routes and ensure they have proper permission checks. Look for any @app.get or @app.post that should be protected but are missing auth.

For example, in FastAPI:

python
@app.get("/admin/users")
def list_users(current_user: User = Depends(get_current_admin_user)):
    ...

Review get_current_admin_user:

Also check resource ownership. For example, retrieving an order:

python
@app.get("/orders/{order_id}")
def get_order(
    order_id: int,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    order = db.get(Order, order_id)
    if not order:
        raise HTTPException(status_code=404, detail="Order not found")
    if order.user_id != current_user.id and not current_user.is_admin:
        raise HTTPException(status_code=403, detail="Not allowed")
    return order

Common authorization mistakes:

  • Relying only on frontend checks.
  • Checking only that a user is authenticated, not that they own the resource.
  • Having one admin route without the admin check because you forgot the dependency.

3. Input Validation and Common Web Vulnerabilities

Your backend must never trust user input. Carefully validate and sanitize it.

Request Validation

Use Pydantic models consistently and set strict types.

Example registration model:

python
from pydantic import BaseModel, EmailStr, constr
class RegisterUser(BaseModel):
    email: EmailStr
    password: constr(min_length=8, max_length=128)
    full_name: constr(strip_whitespace=True, min_length=1, max_length=100)

Do not accept loose dict or Any types without validation.

Ensure you:

Protect Against SQL Injection

Use your ORM or parameterized queries. Never concatenate user input into SQL.

Bad:

python
user_id = request.query_params["user_id"]
query = f"SELECT * FROM users WHERE id = {user_id};"
db.execute(query)

Good (SQLAlchemy):

python
user = db.query(User).filter(User.id == user_id).first()

Or, if you use raw SQL, always use parameters:

python
db.execute(text("SELECT * FROM users WHERE id = :id"), {"id": user_id})

Prevent Cross Site Scripting (XSS)

On a pure API backend, XSS is mostly a concern in:

Rules:

Example in Jinja2, by default:

html
<p>{{ user_comment }}</p>  <!-- Escaped by default -->

If you disable escaping (|safe), you must be absolutely sure the content is sanitized.

Prevent CSRF (Cross Site Request Forgery)

For a JSON API with JWT and Authorization: Bearer headers, CSRF risk is lower. CSRF mainly affects cookie based auth.

If your final project uses cookies for auth:

FastAPI example using a header for CSRF:

Limit Payload Sizes and File Uploads

For file uploads and large JSON bodies, set limits at your app or reverse proxy.

Examples:

nginx
client_max_body_size 10M;
python
from fastapi import Request, HTTPException
MAX_BODY_SIZE = 10 * 1024 * 1024  # 10 MB
async def enforce_body_size(request: Request):
    body = await request.body()
    if len(body) > MAX_BODY_SIZE:
        raise HTTPException(status_code=413, detail="Payload too large")

Add this dependency to routes that receive large bodies.

For uploaded files:

4. Secrets and Configuration

You configured environment variables in earlier chapters. Now check that none of them are exposed or mishandled.

No Secrets in the Repository

Search for patterns like:

Look in:

Bad:

python
JWT_SECRET_KEY = "supersecret123"  # Hard coded
DATABASE_URL = "postgres://user:password@host/db"

Good:

python
import os
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY")
DATABASE_URL = os.getenv("DATABASE_URL")

Ensure:

If you accidentally committed a secret:

  1. Rotate the secret (new key, new password).
  2. Remove it from code.
  3. If public, treat the old value as compromised.

Secure Environment Variables in Production

Check how secrets are stored on the server or orchestrator:

Avoid passing secrets via command line arguments or storing them in shell history.

Rule:
All sensitive configuration values (DB passwords, JWT keys, SMTP credentials, API keys) must come from secure environment variables or a dedicated secrets manager, never from source code or public files.

5. Transport Security and Network Configuration

You must protect data in transit and minimize exposed services.

Enforce HTTPS

Check that:

Example Nginx redirect:

nginx
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

Disable Insecure Protocols and Ciphers

Use a reasonable TLS configuration. For example, with Nginx you might use:

nginx
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;

You can use online generators or guides from Mozilla to pick secure defaults.

Limit Network Exposure

Review what ports are open:

Examples:

conf
  listen_addresses = 'localhost'

or in Docker, only expose port to other containers, not the internet.

conf
  bind 127.0.0.1
  protected-mode yes

Check your firewall (ufw, security groups, etc.). For a simple single server:

PortServiceExposure
22SSHRestricted to your IP if possible
80HTTPPublic (for redirect to HTTPS)
443HTTPSPublic
5432PostgreSQLPrivate (local only / VPC only)
6379RedisPrivate (local only / VPC only)

6. Database and Data Security

Your database contains your most sensitive data. Protect it carefully.

Principle of Least Privilege

The application should use a database user with only the privileges it needs.

Bad:

sql
CREATE USER app_user WITH SUPERUSER PASSWORD '...';

Good:

sql
CREATE USER app_user WITH PASSWORD '...';
GRANT CONNECT ON DATABASE mydb TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;

Ensure:

Encrypt Sensitive Columns Where Appropriate

For very sensitive data (for example payment tokens, personal identifiers beyond emails), consider additional encryption at the application level.

For example, you might:

Example pattern:

python
from cryptography.fernet import Fernet
cipher = Fernet(settings.DATA_ENCRYPTION_KEY)
def store_sensitive(data: str) -> str:
    return cipher.encrypt(data.encode()).decode()
def load_sensitive(encrypted: str) -> str:
    return cipher.decrypt(encrypted.encode()).decode()

Use this for fields like ssn or credit_card_token if you have them. For many projects, avoiding such data entirely is best.

Backups and Data Protection

Check:

Also verify that logs or backups do not contain secrets in plain text beyond what is strictly necessary.

7. Operational Security: Logging, Monitoring, and Incident Handling

Security is not a one time event. You need visibility and a plan for when things go wrong.

Logging Securely

You want enough logs to investigate incidents, but not too much that you store sensitive data.

Check:

Example structured log:

python
logger.info(
    "user_login_failed",
    extra={"email": email, "ip": client_ip, "reason": "invalid_password"},
)

Ensure log files:

Monitoring and Alerts

From the monitoring chapter you saw tools like Prometheus or external services. For security review, check:

Even a basic setup helps. For example, configure:

Rate Limiting and Abuse Protection

Review your rate limiting:

Example conceptual rate limit per IP:

You can implement rate limiting with Redis, storing counters per key:

Key exampleMeaning
login:ip:203.0.113.5Login attempts from this IP
password_reset:email:...Password reset requests per email

If a threshold is reached, return HTTP 429 Too Many Requests.

Incident Response Basics

You do not need a big company process, but you should think through:

Document a simple plan in your SECURITY_REVIEW.md, for example:

A Minimal Pre‑Production Security Checklist

You can adapt this table for your final project and tick items explicitly.

AreaCheckDone
CodeDebug mode and reload disabled in production[ ]
CodeNo stack traces or internal details in API responses[ ]
DependenciesDependencies scanned for vulnerabilities and updated[ ]
AuthPasswords hashed with Argon2/bcrypt, never stored in plain text[ ]
AuthJWTs signed with strong secret, have expiration, verified on every request[ ]
AuthorizationAll sensitive routes require appropriate roles / ownership checks[ ]
Input validationAll request bodies validated with Pydantic, types and sizes checked[ ]
InjectionAll DB access uses ORM or parameterized queries[ ]
XSS / CSRFTemplates escape user data, cookies have SameSite where needed[ ]
File uploadsFile types validated, size limited, stored safely[ ]
SecretsNo secrets in repo, all come from env/secrets manager[ ]
HTTPSHTTPS enabled, HTTP redirected to HTTPS[ ]
NetworkDB and Redis not publicly accessible, firewall configured[ ]
DBApp DB user uses least privilege, no SUPERUSER[ ]
BackupsAutomated DB backups configured and tested[ ]
LoggingSensitive data excluded from logs, important actions logged[ ]
MonitoringBasic monitoring and alerts set up (uptime, error rates, login failures)[ ]
Rate limitingRate limiting implemented for login and other sensitive / expensive endpoints[ ]

As you finalize your production backend, go through each of these checks, adapt them to your architecture, and document what you have verified. This final security review step is what separates a hobby project from something you can confidently deploy to real users.

Views: 7

Comments

Please login to add a comment.

Don't have an account? Register now!