AI engineerCase studyClearPath security review

Securing an AI agent that approves medical requests.

ClearPath is my LangGraph prior-authorization agent, live on Google Cloud Run. I ran a full OWASP Top 10:2025 and OWASP Top 10 for LLM Applications review of it, reproduced the worst findings on the live site, and fixed all of them with tests that prove each fix.

Role
Solo: threat model, review, fixes, deploy
Stack
FastAPI · LangGraph · Supabase Auth + Postgres RLS · Google Cloud Run · Secret Manager · GitHub Actions
When
October 2026; fixes verified on the live service
Proof
Full review (25 findings) · Live app
25Findings, each with file:line evidence
2→0Critical flaws, fixed and verified live
117Authorization-matrix tests
8/8Injection attacks resisted by GPT-4o
01 / The problem

A demo that anyone could steer.

ClearPath reviews prior-authorization requests: it auto-resolves clear approvals and sends every denial and uncertain case to a human reviewer. The whole point is that a human signs off on the hard decisions. The original build had no security layer at all, so before calling it production-ready I reviewed it the way an attacker would.

I started with a threat model: what is worth protecting, who can reach it, and where trust changes hands.

  • Assets: case data (PHI in a real deployment), reviewer decisions and the audit trail, API keys and database credentials, the LLM budget, and the public MCP server.
  • Actors: anonymous visitor, submitter, reviewer, admin, and an attacker trying injection, token forgery, IDOR, header spoofing and cost exhaustion.
  • Trust boundaries: internet to app, app to the LLM (its output is untrusted), app to database, case text to prompt, and CI to cloud.
Reproduced on the live site

Anyone could approve or deny any case "as human reviewer": an anonymous request returned {"finalized":"approve","by":"human reviewer"}. There was no login anywhere. And adding one field, "expected":"approve", to a request got any case auto-approved through the offline planner.

02 / What I changed

Defence in depth, proven by tests.

Browser / MCPuntrusted CLOUD RUN · FASTAPI JWT verify (Supabase Auth) Role check on every route Pydantic validation, 64 KiB cap Rate limit, daily LLM cap Security headers, CSP LangGraphfenced case text OpenAIoutput untrusted Postgres (Supabase)RLS · least privilege · audit log Secret Managerruntime account, no project roles Private GCS bucketdaily pg_dump · restore tested CI on every push: Bandit · Semgrep · pip-audit · gitleaks · Trivy · OWASP ZAP backup
Request path after the review: every layer can refuse on its own

Identity and access

  • Supabase Auth with every JWT verified locally (signature, expiry, audience, issuer; algorithm fixed by config, never taken from the token). Roles come from the database, never from token claims.
  • Roles enforced on every endpoint and MCP tool: only the reviewer assigned to a case can approve or deny it. Admins can assign but cannot decide.
  • Postgres row-level security under the API, with a least-privilege app role and an append-only audit log, so the database itself refuses cross-user reads, forged decisions and edits to the log.

Inputs, AI and cost

  • Pydantic schemas with enums and length limits, unknown fields dropped (which closed the expected hole), a 64 KiB body cap and send-backs capped at 3.
  • Prompt injection: untrusted case text fenced off from instructions, a deterministic tripwire that forces human review on instruction-like text, and schema-validated output. Denials can never be auto-finalized; the route is decided in Python, not by the model.
  • Per-IP rate limits and a daily LLM call cap that fails closed, with the spend counter in Postgres.

Secrets, supply chain and recovery

  • Keys moved to Google Secret Manager; the service runs as an account with no project roles.
  • Hash-pinned dependencies, Dependabot, an SBOM, and seven scanners in CI on every push.
  • Daily pg_dump to a private bucket by a create-only identity, with an automated restore test on every run.
03 / Results

Before and after.

SeverityBeforeOpen afterFixed and verified live
Critical202
High605 (+1 mitigated, accepted)
Medium10010
Low707
CheckBeforeAfter
pytestno tests250 passed (117 authorization, 31 database-level)
pip-audit21 known vulnerabilities in 6 packages0
OWASP ZAP baselinenot run0 High · 0 Medium · 0 Low
Trivy image scannot run0 fixable High/Critical
Injection eval, live GPT-4onot tested8/8 resisted, 8/8 flagged by the model
Accepted risk, stated honestly

Prompt injection can't be fully prevented: a subtle injection without tripwire phrases could still bias the model. The only thing it could win here is an auto-approval, which is capped by the fenced prompt, the tripwire and the schema. A real PHI deployment would also add MFA for reviewers and column-level encryption.

04 / What I'd do next

Where it goes from here.

  • TOTP MFA for reviewers and admins (Supabase supports it; it needs an enrolment screen).
  • Verify JWTs inside Postgres so the database stops trusting the app for identity.
  • Self-host Langfuse or strip model rationales from traces before handling real PHI.
More case studies
Get in touch

Need an AI system that holds up in production?

Tell me what you're building and where it breaks. I'll tell you straight whether I can help.

Cairo, Egypt — US, UK & Gulf time zones LinkedIn GitHub