Skip to content

[Security] Missing Input Validation, Rate Limiting, and Insecure DEBUG Default #501

@prince-shakyaa

Description

@prince-shakyaa

[Security] Missing Input Validation, Rate Limiting, and Insecure DEBUG Default

Description

Three security issues exist in the backend that allow unauthenticated actors to abuse
the magic-link authentication endpoint and cause the application to ship in an insecure
debug mode by default.


Finding 1 — No Email Format Validation on POST /auth/magic-link

File: finbot/apps/finbot/auth.py · Lines 27–36

@router.post("/magic-link")
async def request_magic_link(request: Request, email: str = Form(...)):
    email = email.lower().strip()
    # ← No format check. "not-an-email", "   ", or "a@" all pass through.
    magic_token = MagicLinkToken(token=token, email=email, ...)
    db.add(magic_token)
    db.commit()

Any string (including non-email garbage) is accepted, stored in the MagicLinkToken
table, and forwarded to the email service. This results in:

  • Orphaned token rows accumulating in the database
  • The email service receiving undeliverable addresses
  • Downstream errors that surface unhandled exception details to the user

Proposed fix: Validate using Pydantic's EmailStr (already a project dependency via pydantic[email]):

from pydantic import EmailStr, ValidationError

try:
    EmailStr._validate(email)
except Exception:
    return template_response(request, "auth-error.html", {
        "error": "Invalid email",
        "message": "Please enter a valid email address.",
    })

Finding 2 — No Rate Limiting on POST /auth/magic-link

File: finbot/apps/finbot/auth.py · Lines 27–81

There is no throttle on the magic-link endpoint. Any unauthenticated actor can
call it in a tight loop, which:

  • Floods a victim's inbox with magic-link emails (email harassment / spam abuse)
  • Creates unbounded rows in the MagicLinkToken table (slow database DoS)
  • Exhausts the email provider's quota (especially critical with Resend in production)

The LLM chat endpoint POST /vendor/api/v1/chat is also unthrottled, but that is
out of scope for this issue.

Steps to reproduce:

for i in $(seq 1 20); do
  curl -s -X POST http://localhost:8000/auth/magic-link \
    -d "email=victim@example.com" -o /dev/null
done
# 20 magic-link emails sent to victim with no server-side pushback

Proposed fix: A lightweight per-IP sliding-window limiter (5 requests / 60 s) —
no new dependencies required:

import time
from collections import defaultdict
from threading import Lock

_RATE_LIMIT_WINDOW = 60   # seconds
_RATE_LIMIT_MAX    = 5    # requests per window
_rate_store: dict[str, list[float]] = defaultdict(list)
_rate_lock = Lock()

def _is_rate_limited(ip: str) -> bool:
    now, cutoff = time.monotonic(), time.monotonic() - _RATE_LIMIT_WINDOW
    with _rate_lock:
        _rate_store[ip] = [t for t in _rate_store[ip] if t > cutoff]
        if len(_rate_store[ip]) >= _RATE_LIMIT_MAX:
            return True
        _rate_store[ip].append(now)
        return False

Note: For multi-worker deployments, replace with a Redis-backed limiter
(e.g. slowapi + limits) so the counter is shared across processes.


Finding 3 — DEBUG: bool = True Is the Default Value

File: finbot/config.py · Line 49

class Settings(BaseSettings):
    DEBUG: bool = True   # ← Ships as True by default

When DEBUG=True the server starts with reload=True (hot-reload), log level is
set to "debug", and verbose tracebacks may be exposed. Any developer who clones the
repo and forgets to set DEBUG=false in .env silently runs in debug mode.

Proposed fix: Default to False and document how to enable it for local dev:

DEBUG: bool = False  # Override with DEBUG=true in .env for local dev

Steps to Reproduce

Finding 1 — Invalid email accepted:

  1. Start the app locally
  2. POST /auth/magic-link with body email=not-an-email
  3. Observe: no validation error, token is written to DB, email service is called

Finding 2 — No rate limit:

  1. Start the app locally
  2. Run the curl loop above 10+ times in rapid succession
  3. Observe: all requests succeed, no 429 response

Finding 3 — DEBUG ships on:

  1. Clone the repo, do not create a .env file
  2. Run python run.py
  3. Observe: server starts with reload=True and log_level=debug

Affected Files

File Line Issue
finbot/apps/finbot/auth.py 27–36 No email format validation
finbot/apps/finbot/auth.py 27–81 No rate limiting
finbot/config.py 49 DEBUG: bool = True default

Labels

bug · security · backend · good first issue

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions