← Blog

How I Fixed Five API Vulnerabilities with One Prompt and Claude Fable 5

AI/LLM Security Engineering

Behind the VLOP transparency dashboard sits a small FastAPI service — a structured-query API modelled on the TikTok Research API, serving aggregated EU DSA transparency data. Several of its endpoints are deliberately public: an interactive query builder, a natural-language "ask" box, an overview feed. Public endpoints on a hobby-scale service are exactly where security debt accumulates quietly, because nothing forces you to look.

So I asked Claude Code — running Fable 5, the first model in Anthropic's Claude 5 family — to look. The prompt was one line: "run a security check over the API and fix any vulnerabilities." What came back was a pull request with five distinct fixes, each with regression tests, plus a self-review of its own diff.

What it found

An SSRF gap in webhook callbacks. The API lets a query carry a callback_url that gets POSTed when the job finishes — a classic server-side request forgery surface. The existing guard blocked private, loopback, link-local, and cloud-metadata ranges, but block-listing is the wrong shape for this problem: it missed ranges that are neither private nor public, like carrier-grade NAT (100.64.0.0/10). The fix inverts the check — a callback target must now be globally routable, or it's rejected.

CSV formula injection. Query results export to CSV, and the dataset includes free-text fields sourced from third-party transparency reports. A cell starting with =, +, -, or @ executes as a formula when the file opens in Excel or Sheets — meaning a value in someone else's published report could run code on a researcher's machine. Text cells with those prefixes are now neutralised, both server-side and in the dashboard's client-side export.

Unbounded request bodies and query complexity. The public query endpoint accepts the full structured-query model without authentication. Nothing capped how large a request body could be or how many conditions a query could stack. Bodies over a limit are now rejected with a 413 before any parsing work, and the query model itself bounds values per condition, conditions per clause, and fields per request.

A timing side channel in API key checks. Keys were checked with a dictionary lookup, which short-circuits on the first differing character — in principle, response timing leaks key prefixes. Comparison is now constant-time over every configured key.

Plus assorted hardening — the kind of small-bore items a checklist sweep catches and a busy maintainer doesn't: bounds on pagination parameters, that sort of thing.

The part that surprised me

The pull request triggered an automated review from a second AI, Gemini Code Assist, which left two suggestions. Fable 5 evaluated both against the actual source before acting — and on one of them, it found the bot's reasoning incomplete. Gemini flagged that parsing an attacker-supplied Content-Length header of arbitrary length into an integer could burn CPU. True but minor. The sharper problem, which Fable 5 identified and explained in its reply, is that Python 3.11+ refuses to parse integers beyond ~4,300 digits and raises an exception instead — so a pathological header would have produced a 500 error rather than the intended 413. It also generalised Gemini's hard-coded fix so the bound tracks the configured limit, and added a regression test sending a 5,000-digit header. One AI reviewing another AI's review of the first AI's code, and being right about the disagreement, was not on my bingo card for 2026.

What I take from it

This rhymes with what I wrote about LLMs in compliance work: the wins come on structured tasks with verifiable outputs. A security sweep of a small codebase is exactly that — a finite checklist (injection surfaces, SSRF, side channels, resource exhaustion) applied exhaustively, where every finding can be confirmed by reading the code and every fix can be pinned with a test. Exhaustive-and-boring is where models now beat tired humans reliably.

The judgment stayed with me. Deciding that the metrics endpoint can stay unauthenticated, that demo keys are acceptable outside production, that an in-memory job store is fine for a demo service — those are threat-model calls about what the service is for, and the model didn't pretend otherwise. The full diff, tests included, is in the pull request; the suite runs 122 tests and is green.

esc