PokéAPI got a B. That's both better and worse than it sounds.
About This API
PokéAPI is a public REST API serving structured Pokémon data — species, moves, abilities, evolution chains, types, sprites, locations, pokédex entries, and the rest of the franchise catalog up through the latest generation. It has been running since 2014 and currently handles an order of magnitude more traffic than the project's maintainers originally designed for. The entire service is free, unauthenticated, CDN-cached, and holds no user state. The typical consumer is a developer building a Pokédex app as a portfolio project, a YouTube or Udemy tutorial demonstrating data fetching in React / Vue / Flutter, a Discord bot that answers /typechart fire water, or an indie hobby game scraping the dataset to bootstrap content.
Because the API is a read-only reference dataset, its threat model is fundamentally different from an API that holds customer data. There is no authentication gate to bypass, no user resource to escalate into, no private object to enumerate — every response is intended to be public. The scanner's reports of "missing authentication" and "IDOR via sequential IDs" are literally true but not meaningfully exploitable here. PokéAPI shipping 200 OK on GET /api/v2/pokemon/1 without a token is the feature.
What matters for this class of API is a different question: is it safe to build on top of? The answer is mostly yes, but with three non-obvious caveats. First, the API doesn't send rate-limit headers, so your app cannot implement polite backoff from the response alone — you can and will hammer it if your side project goes mildly viral. Second, response payloads for popular endpoints (moves arrays on late-generation Pokémon, evolution chains with branches, ability lists) can run 85+ items and several hundred kilobytes, which matters on mobile data plans and low-RAM devices. Third, the /api/v1/ surface still returns 200 OK years after v2 was introduced, so any client still pointed at v1 is on a deprecated path that won't see new data or security fixes.
Those three caveats are what a Pokédex-app developer should actually care about from this audit. The rest is scanner noise that's still worth explaining so you don't waste time worrying about the wrong things — or worse, filing an issue against pokeapi/pokeapi that starts with "Critical: missing authentication."
Threat Model
The realistic adversary against a public reference API like PokéAPI is not a data thief — there is nothing private to steal. The threats are (a) bulk abuse of the API as a free bandwidth and CDN-cache channel, (b) downstream exploitation of applications that trust the API's responses without sanitization, and (c) operational pressure on the maintainers that degrades availability for legitimate consumers.
Scraping and bandwidth abuse
Because the API is open and cached at the edge, it is attractive for bulk scraping. A scraper mirroring the entire dataset walks /pokemon/1 through /pokemon/1025, /move/1 through /move/919, and so on — roughly 20,000 endpoints. Without rate-limit headers or authentication, there is no protocol-level mechanism to distinguish scraper traffic from consumer apps. The cost is not the Pokémon data (it is already public and mirrored in the pokeapi/api-data GitHub repo); the cost is bandwidth and cache churn, which the maintainers cover as volunteers. This has happened multiple times historically and has pushed PokéAPI to publish a fair-use policy requesting that heavy consumers either aggressively cache or self-host the dataset via the documented GraphQL mirror.
Response-trust and sprite supply chain
Applications consuming PokéAPI often display sprite image URLs, move flavor text, and pokédex entries directly to end users. Image URLs point to raw.githubusercontent.com/PokeAPI/sprites/ paths. A client that renders these as <img src=""> without a Content Security Policy inherits whatever that repo serves. The probability of a sprite poisoning is low — the repo is public and watched — but the blast radius touches every downstream Pokédex app. Defense is one Content-Security-Policy directive on your side, or mirroring the sprites you actually need.
Client cascading failure
The concrete user-facing risk is that a consuming application writes no defensive client behavior. If PokéAPI slows down or returns a stale CDN response, a Pokédex app that blocks rendering on /pokemon/{id} freezes on its loading spinner. If the response for a legendary Pokémon's moves list exceeds what the app allocates for its parser, a naive implementation OOMs on a low-end phone. These aren't PokéAPI's problems — they are consumer problems caused by assuming a public dependency is always fast, always small, and always available. The absence of pagination signals and rate-limit metadata makes a robust client harder to write but not impossible; it just requires the developer to know about it.
Methodology
middleBrick ran a black-box scan against https://pokeapi.co/api/v2/pokemon/1 — the canonical endpoint documented in PokéAPI's getting-started guide. Twelve security checks executed across OWASP API Top 10 categories: authentication, authorization (BOLA / BFLA / property-level), input validation, CORS, rate limiting, data exposure, encryption, SSRF surface, inventory management, and unsafe consumption. All requests were GET or HEAD, read-only, and obeyed the documented fair-use guidance.
Because some consumer apps wrap PokéAPI behind an LLM — chatbots that look up stats in response to user messages — the scanner additionally fingerprinted the response for LLM endpoint signals: chat-completion paths, model fields, usage metadata, prompt-leakage patterns. None matched. PokéAPI is a straightforward REST API, not an inference endpoint, and the AI-specific probes returned zero hits.
The scan included one active IDOR probe: after a 200 response on /pokemon/1, the scanner requested /pokemon/2 and compared the JSON schemas. A 21-of-21 key match was expected and observed, which is the basis for the "confirmed IDOR" finding. In a private API, a schema match across adjacent IDs would be strong evidence that authorization is not tied to the identifier. In a public reference API, a schema match is the product specification. We report the finding as the scanner detected it, and then in the detailed analysis we explain why it does not translate into an exploit here.
No authenticated-scanning headers were sent, no credentials were probed, and no destructive methods were issued. Even though the OPTIONS response advertised DELETE, PUT, and PATCH as allowed, the scanner never attempted them — method advertisement and method execution are different signals, and middleBrick stops at advertisement for read-only public endpoints.
Results Overview
PokéAPI earned a grade of B with an overall score of 76. Twelve findings were reported across the twelve active security checks — essentially one finding per category. The distribution: two critical, two high, four medium, four low.
Of the two critical findings, both fall into the category we'd call "technically accurate but not an exploit against a public reference API." The first is missing authentication on GET /api/v2/pokemon/1 — which is intentional. The second is sequential-ID IDOR — confirmed via the /pokemon/1 vs /pokemon/2 schema-match probe, and again, intentional; these IDs correspond to the Pokédex number that every Pokémon fan has memorized. If PokéAPI had any non-public data in responses, the same finding would be catastrophic. It does not. We note these findings rather than dismiss them because any scanner a security team runs will flag the same two things, and the operator needs language to explain why they are by design.
The two high-severity findings are more interesting. Wildcard CORS (Access-Control-Allow-Origin: *) is, like the critical findings, deliberate for a public read-only API — it's what lets your tutorial's fetch() call work from any origin. Security scanners flag it because the same configuration would be a browser-side data-leak vector on an authenticated API. It is not one here. The more consequential high-severity finding is no rate-limit headers: no X-RateLimit-*, no Retry-After. PokéAPI does rate-limit (their documented ceiling is 100 requests per minute and the CDN softens bursts), but the limits aren't advertised in the response. That means a well-behaved client cannot automatically back off — it has to guess.
The four medium findings: (1) a numeric ID in the response body (trivial enumeration signal, already discussed), (2) an unpaginated collection — the moves array held 86 items in our probe — which is a real concern for mobile clients, (3) a "secret value in response body" hit, which is a false positive triggered by the sprite hash URLs pointing at the sprites repo, and (4) multiple external URLs in the response (85 links to raw.githubusercontent.com) — the sprite and cries audio references. The external-URL finding is structural to how PokéAPI distributes its assets, but it is worth knowing about if you are threat-modeling your own CSP.
The four lows: a v1 surface still responding, missing two of four common security headers (X-Content-Type-Options, X-Frame-Options), DELETE/PUT/PATCH advertised via OPTIONS, and an X-Powered-By: Express reconnaissance leak. None are urgent; all are easy operator wins.
Detailed Findings
API accessible without authentication
The endpoint returned 200 without any authentication credentials.
Implement authentication (API key, OAuth 2.0, or JWT) for all API endpoints.
Sequential IDs with confirmed IDOR (ID +1)
Path contains numeric IDs (1) — easily enumerable by attackers. Changing ID from 1 to 2 returned 200. Response schemas match (21/21 keys identical) — strong IDOR evidence.
Use UUIDs or non-sequential identifiers. Implement object-level authorization checks.
CORS allows all origins (wildcard *)
Access-Control-Allow-Origin is set to *, allowing any website to make requests.
Restrict CORS to specific trusted origins. Avoid wildcard in production.
Missing rate limiting headers
Response contains no X-RateLimit-* or Retry-After headers. Without rate limiting, the API is vulnerable to resource exhaustion attacks (DoS, brute force, abuse).
Implement rate limiting (token bucket, sliding window) and return X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After headers.
IDOR risk: numeric object ID in response body
Response body contains 1 numeric ID field(s) (id:1). Enumerable object IDs enable IDOR / insecure direct object reference attacks when per-object authorization is missing.
Use UUIDs for object identifiers. Verify per-object authorization on every request. Never expose internal IDs for resources the user doesn't own.
Large unpaginated collection in response
Response contains large arrays: $.moves (86 items). No pagination metadata detected.
Use cursor-based pagination. Limit collection size per response.
Secret value in response body
Response body contains secret values. This constitutes a PII/sensitive data leak (CWE-200).
Remove or mask sensitive data before returning to clients. Implement field-level access controls and output filtering.
Multiple external URLs in API response
Response references 85 external URLs across 1 host(s): raw.githubusercontent.com
Validate and sanitize all external URLs. Implement allowlists for trusted third-party services.
Missing security headers (2/4)
Missing: X-Content-Type-Options — MIME sniffing; X-Frame-Options — clickjacking.
Add the following headers to all API responses: x-content-type-options, x-frame-options.
Dangerous HTTP methods allowed: DELETE, PUT, PATCH
The server advertises support for methods that can modify or delete resources.
Only expose HTTP methods that are actually needed. Disable TRACE, and restrict DELETE/PUT/PATCH.
Older API version still accessible
https://pokeapi.co/api/v1/pokemon/1 returned 200. Older API versions may lack security fixes.
Decommission old API versions or ensure they receive the same security updates.
Technology exposed via X-Powered-By: Express
The X-Powered-By header reveals framework details.
Remove the X-Powered-By header in production.
Attacker Perspective
An attacker approaching PokéAPI with exploitation in mind quickly runs into the same wall every researcher does: there is nothing to exfiltrate. The dataset is public, the code is on GitHub, the sprites are in a sibling repo. The attacker's realistic goals are therefore narrower and more prosaic.
Abuse the API as a free compute channel
The most practical "attack" is hostile scraping at a volume that forces the maintainers to tighten their fair-use policy or introduce authentication. The absence of rate-limit headers makes this easier, not harder — without Retry-After the scraper cannot distinguish between being throttled and the CDN being slow, so the scraper defaults to aggressive concurrency. A typical pattern is a machine-learning team grabbing the full move effectiveness table for a model-fine-tuning dataset and writing the scraper in the five minutes between "we need this" and "we're running it." The cost falls on PokéAPI's hosting bill.
Poison a client through the sprite supply chain
A more imaginative attacker goes after consuming applications rather than the API itself. The /pokemon/{id} response embeds URLs to raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{id}.png. An attacker who (a) gains commit access to the sprites repo, (b) convinces a maintainer to merge a poisoned PR, or (c) chains a reverse-proxy misconfiguration in front of GitHub's raw CDN, can serve non-image content on a sprite path. A client that uses the URL with <img src> and no CSP renders whatever is served; a client using fetch() and URL.createObjectURL inherits the MIME type. This is an extremely low-probability chain, but it is the specific reason a Pokédex app should set img-src in its CSP to a tight allowlist and prefer self-hosted or SRI-pinned assets for production.
Embed malicious flavor text
Pokédex entries and move flavor text are strings pulled from the underlying dataset. A client that renders them via innerHTML (or a React dangerouslySetInnerHTML) inherits whatever characters the dataset contains. The PokéAPI dataset is currently clean, but this is a zero-cost precaution: render Pokédex entries as text nodes, never as HTML. Any tutorial that teaches dangerouslySetInnerHTML on PokéAPI output is training a bad habit.
Request-shape reconnaissance
The X-Powered-By: Express header and the /api/v1/ legacy surface do not themselves enable an attack, but they narrow the fingerprint. A researcher mapping third-party API dependencies of a large target app can use these signals to correlate cross-service traffic. The value is small; the cost of suppressing them is smaller.
Analysis
The two critical findings are the scanner's honest reading of the HTTP contract. GET /api/v2/pokemon/1 returns 200 OK without an Authorization header; that is "missing authentication." A subsequent GET /api/v2/pokemon/2 returns the same 21-field schema; that is "sequential IDs with confirmed IDOR." Both are accurate; neither is exploitable, because the entire dataset is intended to be public.
The rate-limit finding is more actionable. A request to /pokemon/1 returns these headers:
HTTP/2 200
server: cloudflare
cache-control: public, max-age=86400, s-maxage=86400
content-type: application/json; charset=utf-8
x-powered-by: Express
access-control-allow-origin: *There is no X-RateLimit-Limit, no X-RateLimit-Remaining, no Retry-After. PokéAPI's documented ceiling is 100 requests per minute, but a consumer has no programmatic way to know when they're about to hit it. A well-behaved client fills this gap with a client-side token bucket:
const bucket = { tokens: 100, refillAtMs: Date.now() + 60_000 };
async function safeFetch(url) {
if (Date.now() > bucket.refillAtMs) {
bucket.tokens = 100;
bucket.refillAtMs = Date.now() + 60_000;
}
if (bucket.tokens <= 0) {
await new Promise(r => setTimeout(r, bucket.refillAtMs - Date.now()));
return safeFetch(url);
}
bucket.tokens--;
return fetch(url);
}The CORS wildcard shows up as access-control-allow-origin: *. Critically, the API does not set access-control-allow-credentials: true, which means no browser will send cookies or session auth to PokéAPI cross-origin. That's the safe configuration for a public read-only API. The scanner flagged it as high because the same * on an authenticated API would be a browser-side data-leak vector. Context matters: on PokéAPI this is correct; on a fintech API it would be a disaster.
The large-response finding is concrete. For Pokémon #1 (Bulbasaur), the moves array held 86 entries in the scan response. For late-generation Pokémon the number climbs higher, and each entry is an object with nested URLs pointing at further endpoints. A React client that sets moves into state and renders them unpaginated triggers 86 child components and potentially 86 follow-up fetches for move detail. On mobile this is visible as jank. The defensive client code is to paginate in the UI (virtualized lists) and lazy-load the move details on demand — none of which PokéAPI can signal via pagination metadata because the endpoint doesn't emit any.
The "secret value in response body" finding is a false positive caused by the sprite hash URL — our data-exposure check looks for entropy patterns consistent with API tokens, and long hex-looking paths in URLs occasionally trip it. We flag it because the scanner reported it, not because it's real. If you see the same finding on your own API, double-check against your real response shape before treating it as a leak.
Industry Context
PokéAPI is the reference example for a whole class of API: the fan-built, publicly-hosted, volunteer-maintained REST endpoint that powers thousands of downstream projects. Other members of the class include Rick and Morty API, SWAPI (Star Wars), The One API (Lord of the Rings), and to a lesser extent Open Library and the Wikipedia REST API. All of them share the same posture: open, unauthenticated, CDN-cached, rate-limited by policy rather than by HTTP, and sensitive to bandwidth abuse.
A security scanner pointed at any of these will produce a similar report card to PokéAPI's: critical-severity flags on authentication and sequential IDs that are not exploits, and the real findings — rate-limit headers, response size, version management, security headers — hiding further down. Compliance frameworks are largely silent on this class because the data is not regulated: PokéAPI does not touch PCI-DSS, HIPAA, or GDPR personal-data scope. OWASP API Top 10 still applies technically, but the practical interpretation shifts: API1 (broken auth) and API3 (broken object-property-level authorization) don't apply where there is no auth model to break; API4 (unrestricted resource consumption) is the one that actually bites.
For a developer auditing their Pokédex app's third-party dependencies, the actionable takeaway is to treat PokéAPI as a best-effort dependency rather than a contracted service. That means: cache aggressively on your side, fail open on degraded responses, never expose PokéAPI URLs directly to your users where a response-trust issue could propagate, and plan for the day PokéAPI announces a breaking change or a mandatory CDN migration.
Remediation Guide
No rate-limit headers
Emit X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After on every response. Gives consumers protocol-level information to back off politely and distinguishes throttling from CDN latency.
// Express + rate-limiter-flexible
import { RateLimiterMemory } from 'rate-limiter-flexible';
const limiter = new RateLimiterMemory({ points: 100, duration: 60 });
app.use(async (req, res, next) => {
try {
const r = await limiter.consume(req.ip);
res.set({
'X-RateLimit-Limit': 100,
'X-RateLimit-Remaining': r.remainingPoints,
'X-RateLimit-Reset': Math.ceil(r.msBeforeNext / 1000)
});
next();
} catch (r) {
res.set('Retry-After', Math.ceil(r.msBeforeNext / 1000));
res.status(429).json({ error: 'rate limited' });
}
}); Unpaginated moves array (86+ items)
Either paginate on the endpoint (add ?limit/?offset query params) or emit cursor metadata. If the response shape must stay compatible, add a companion endpoint (/pokemon/{id}/moves?limit=20) and document it for heavy consumers.
// Cursor-style pagination companion endpoint
app.get('/api/v2/pokemon/:id/moves', (req, res) => {
const { id } = req.params;
const limit = Math.min(Number(req.query.limit ?? 20), 100);
const offset = Number(req.query.offset ?? 0);
const all = getMovesFor(id);
res.json({
count: all.length,
next: offset + limit < all.length ? `?offset=${offset + limit}&limit=${limit}` : null,
results: all.slice(offset, offset + limit)
});
}); Legacy /api/v1/ surface still answering
Issue 301 redirects from /api/v1/* to the equivalent /api/v2/* path, or return 410 Gone with a documentation link. Either pushes consumers onto the supported code path.
app.use('/api/v1', (req, res) => {
const v2Path = req.path; // identical under v2
res.redirect(301, `/api/v2${v2Path}`);
}); X-Powered-By reveals framework
Disable the default Express header. One line.
app.disable('x-powered-by'); Missing X-Content-Type-Options, X-Frame-Options
Apply standard defensive headers via middleware (helmet is the canonical choice).
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: false, // JSON API, no HTML surface
frameguard: { action: 'deny' },
noSniff: true
})); DELETE/PUT/PATCH advertised via OPTIONS
If the server rejects mutating methods with 405, update the OPTIONS response to only advertise the methods that actually succeed.
app.options('/api/v2/pokemon/:id', (req, res) => {
res.set({
'Allow': 'GET, HEAD, OPTIONS',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS'
}).sendStatus(204);
}); Client cascading failure when the dependency slows
On the consumer side: add a timeout, a retry budget, and a graceful fallback. Never block primary rendering on a third-party public API.
async function fetchWithFallback(url, cached) {
const ac = new AbortController();
const to = setTimeout(() => ac.abort(), 3000);
try {
const r = await fetch(url, { signal: ac.signal });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return await r.json();
} catch {
return cached; // serve last-known-good
} finally {
clearTimeout(to);
}
} Defense in Depth
If you run a public reference API — on the operator side — the highest-leverage defenses are not about authentication, because adding auth breaks the use case. They are about giving consumers the metadata they need to be polite. Send X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After headers on every response; consumers want to behave, you just have to tell them how. Return X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer to reduce the reconnaissance value of your traffic. Suppress X-Powered-By and any framework-default signatures. Publish a machine-readable fair-use policy at a known path (/.well-known/limits.json is a reasonable convention) so consumers can discover it without reading blog posts.
If you consume a public reference API — on the client side — the defenses are about not letting the public dependency take your product down. Cache every response for at least its documented Cache-Control: max-age, ideally longer. Build a client-side rate-limiter that assumes the documented ceiling and spreads requests evenly. Render all text from the API as text nodes, never as HTML. Scope your CSP's img-src tightly and prefer self-hosted or SRI-pinned assets for any image your product displays in a critical flow. Monitor your dependency's status page (PokéAPI's is pokeapi.co/about plus the project's GitHub) and have a documented fallback for when it is down.
On both sides, the operational discipline is the same: version aggressively, deprecate publicly, and retire old surfaces on a schedule. PokéAPI's /v1 surface still answering 200 OK is a missed opportunity; a 410 Gone or a 301 Moved Permanently to /v2 would push any remaining consumer onto the supported code path and close the security-fix gap.
Conclusion
PokéAPI is a B-grade public API, which in context is about as well-configured as a volunteer-maintained free-to-use reference service can reasonably be. Four of the twelve findings are technically correct but not exploits — they are the scanner reading the product specification as if it were a vulnerability. The eight that remain range from easy operator wins (suppress the X-Powered-By header, return a 301 on /v1) to real architectural asks (emit rate-limit headers so clients can back off politely) to signals the consumer app has to handle itself (pagination, CSP, response trust).
The takeaway for a developer building a Pokédex app is that the security work isn't on PokéAPI's side — it's on yours. Cache, paginate client-side, lock down your CSP, and assume the dependency can go slow. The takeaway for someone operating a PokéAPI-class API is that the right controls are the ones that help your consumers behave well, not the ones that gate access to data that is already public.
The takeaway for a security team watching one of their developers wire up a public reference API is to know which findings to take seriously. An auto-generated "PokéAPI has a CRITICAL authentication vulnerability" ticket will bury the three findings that actually matter for the product. This case study is written so the three can be found quickly: rate-limit headers missing, response arrays unbounded, legacy version surface still responding. Those are the ones worth a conversation.