[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:
- Start the app locally
POST /auth/magic-link with body email=not-an-email
- Observe: no validation error, token is written to DB, email service is called
Finding 2 — No rate limit:
- Start the app locally
- Run the
curl loop above 10+ times in rapid succession
- Observe: all requests succeed, no 429 response
Finding 3 — DEBUG ships on:
- Clone the repo, do not create a
.env file
- Run
python run.py
- 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
[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-linkFile:
finbot/apps/finbot/auth.py· Lines 27–36Any string (including non-email garbage) is accepted, stored in the
MagicLinkTokentable, and forwarded to the email service. This results in:
Proposed fix: Validate using Pydantic's
EmailStr(already a project dependency viapydantic[email]):Finding 2 — No Rate Limiting on
POST /auth/magic-linkFile:
finbot/apps/finbot/auth.py· Lines 27–81There is no throttle on the magic-link endpoint. Any unauthenticated actor can
call it in a tight loop, which:
MagicLinkTokentable (slow database DoS)The LLM chat endpoint
POST /vendor/api/v1/chatis also unthrottled, but that isout of scope for this issue.
Steps to reproduce:
Proposed fix: A lightweight per-IP sliding-window limiter (5 requests / 60 s) —
no new dependencies required:
Finding 3 —
DEBUG: bool = TrueIs the Default ValueFile:
finbot/config.py· Line 49When
DEBUG=Truethe server starts withreload=True(hot-reload), log level isset to
"debug", and verbose tracebacks may be exposed. Any developer who clones therepo and forgets to set
DEBUG=falsein.envsilently runs in debug mode.Proposed fix: Default to
Falseand document how to enable it for local dev:Steps to Reproduce
Finding 1 — Invalid email accepted:
POST /auth/magic-linkwith bodyemail=not-an-emailFinding 2 — No rate limit:
curlloop above 10+ times in rapid successionFinding 3 — DEBUG ships on:
.envfilepython run.pyreload=Trueandlog_level=debugAffected Files
finbot/apps/finbot/auth.pyfinbot/apps/finbot/auth.pyfinbot/config.pyDEBUG: bool = TruedefaultLabels
bug·security·backend·good first issue