Skip to content
Module 6 · Production MasteryAdvanced6 min read · 1,184 words

Lesson 25: Security

A source-grounded audit — what's actually protected, what isn't, and why some gaps matter more than others.

No Authentication, No Authorization — Confirmed, Not Assumed

main.py mounts two routers and two health-check endpoints; nothing else. There is no Depends(get_current_user) anywhere, no API key check, no session cookie, no JWT verification — all 20 endpoints in agents.py and documents.py are reachable by anyone who can send an HTTP request to the server. Lesson 11 covers how you'd add authentication; this lesson is about what the absence of it actually exposes today:

  • Anyone can create, read, or (via the broken /stats endpoint, see Lesson 09a) attempt to delete any agent.
  • Anyone can read any agent's full chat history, task list, uploaded documents, and cost data — there's no concept of "this agent belongs to this user."
  • agent_has_tool() gates capabilities (is email analysis enabled for this agent) but nothing gates access (is the caller allowed to talk to this agent at all).

No CORS Middleware

python
app = FastAPI(title=" AgentOps AI", description="Backend API for AgentOps AI project", version="0.1.0")
app.include_router(agents.router)
app.include_router(documents.router)

No from fastapi.middleware.cors import CORSMiddleware, no app.add_middleware(...) call at all — this is genuinely absent, not misconfigured. In production, a browser running JavaScript from a different origin than the backend would be blocked from calling it directly by the browser's same-origin policy, which is why CLAUDE.md documents that the Next.js frontend proxies through server-side app/api/**/route.ts handlers rather than calling localhost:8001 from client-side code — the browser only ever talks to the Next.js origin, and Next.js's server (not subject to CORS) talks to FastAPI. This architecture happens to make the missing CORS middleware currently moot for the frontend that exists — but it means the FastAPI backend itself has zero built-in defense if something ever does need to call it directly from a browser.

File Upload Validation Is Shallow

From Lesson 09b: the only checks on an uploaded file are (1) the filename ends in .txt, and (2) the bytes decode as UTF-8. There is no file size limit — await file.read() loads the entire upload into memory regardless of size, which is both a performance concern (Lesson 24) and a potential denial-of-service vector (a very large upload consumes memory and, per chunk, real OpenAI API budget, before any size check could reject it). There's also no rate limiting on this or any other endpoint — nothing stops a single caller from uploading thousands of documents back to back, each one burning real API cost.

SQL Injection: Not a Realistic Risk Here

Worth stating plainly rather than implying every category of vulnerability applies equally: every database query in this codebase goes through SQLAlchemy's ORM query builder (db.query(models.Agent).filter(models.Agent.id == agent_id)), which parameterizes values automatically. There is no raw string-formatted SQL anywhere in routers/ or services/ — the one place raw SQL text appears is database.py's run_startup_migrations, and the interpolated values there (column_name, column_type) come from a hardcoded Python dict, not user input. SQL injection is not a live risk in this codebase as written.

Secrets Handling

OPENAI_API_KEY is read via os.getenv in three separate service files (openai_service.py, document_service.py, multi_agent_email_service.py), each independently calling load_dotenv() — see Lesson 27 for why the repetition itself is a maintainability issue. On the "is it handled safely" question specifically: backend/.env is correctly excluded via .gitignore (confirmed: .env, .env.*, with !.env.example as the one allowed exception) and is not tracked in git. The key never appears in a response body or a ToolCall.tool_output anywhere in the reviewed code. The one place a raw exception message could theoretically leak internal detail is the str(e) pattern documented in Lesson 22 — not the API key specifically, but a general information-disclosure surface worth the same caution.

Cross-Agent Data Leakage Surface (ChromaDB)

Covered in depth in Lesson 16: every agent's document chunks live in one shared ChromaDB collection, and isolation is enforced entirely by a where={"agent_id": agent_id} filter passed at query time — not by the database schema. Every current call site passes this filter correctly, so this is not an active leak today, but it's a single-point-of-failure isolation model: one missed filter in one future code path would leak every agent's uploaded documents to every other agent, with no secondary defense.

Task Ownership: Verified Gap

Also noted in Lesson 09a: PUT /tasks/{task_id}/status looks up a task purely by task_id, with no check that the task belongs to any particular agent, let alone any particular authenticated caller (which doesn't exist). Combined with no authentication, anyone who can guess or enumerate a task_id (small sequential integers, easily enumerable) can change its status.

Ranked Summary

FindingSeverity todayWhy
No authentication/authorizationHighEvery endpoint and every agent's data is fully open
Sequential integer IDs, enumerable everywhereMediumCompounds the above — no auth and guessable IDs
No upload size limitMediumReal memory and API-cost DoS surface
str(e) in 500 responsesLowReal, but limited information disclosure
ChromaDB shared-collection isolationLow todaySingle point of failure, not currently broken
SQL injectionNot applicableORM parameterizes everywhere

Common Mistakes

  • Assuming the missing CORS middleware means the API is "protected." It only affects browser-based cross-origin JavaScript — it does nothing against a server-to-server request, curl, or any non-browser client.
  • Treating "no SQL injection risk" as "no security issues." The two are unrelated — this codebase has real gaps in authentication and resource limits despite being safe from SQL injection specifically.

Exercises

  1. Add a MAX_UPLOAD_SIZE constant and enforce it in upload_document before await file.read() fully consumes the request body — return 413 for anything over the limit.
  2. Sketch the minimal authentication addition from Lesson 11 plus one authorization check (e.g., a user_id column on Agent, checked against the authenticated caller) that would close the "anyone can read any agent's data" gap.
  3. Add basic rate limiting (even an in-memory, single-process counter keyed by IP, acceptable for this codebase's current scale) to the document upload and email analysis endpoints, and justify why those two specifically are the highest-value targets for it.

Key Takeaways

🎯 Zero authentication or authorization exists — every endpoint and every agent's data is open to any caller. 🎯 CORS is entirely unconfigured; the frontend's Next.js proxy currently makes this moot, but the backend itself has no defense if called directly. 🎯 SQL injection is not a real risk — the ORM parameterizes every query in this codebase. 🎯 File uploads have no size limit and no rate limiting — a real, low-effort denial-of-service and cost-exhaustion surface.

Next Steps

👉 Lesson 26: Caching →


Module 6 · Production Mastery