Security Audit Report

Rick and Morty API Security Audit: 9 Findings and a Propagated URL Pattern

https://rickandmortyapi.com/api/character/1
78 B Good security posture
9 findings
2 Critical 2 High 2 Medium 3 Low
Share
01

About This API

The Rick and Morty API (rickandmortyapi.com) is a free, no-auth REST + GraphQL service that returns canonical data about every character, location, and episode in the show. It exists because the maintainer (afuh on GitHub) wanted to build the show-themed dataset that everyone always wants to build, and because public-data APIs of this shape are an excellent teaching tool for fetching, pagination, schema design, and React-Query / TanStack-Query tutorials. It launched in 2017 and has been continuously maintained since.

The reach is striking for a fan project. It is the dataset behind hundreds of React-Query tutorials, the example endpoint in TanStack's official docs, the demo target in countless Vue Router tutorials, and the de-facto 'real public API' for any developer wanting to teach client-side caching against something more interesting than JSONPlaceholder. Tutorial volume is high enough that the API's own status dashboard shows weekly request peaks in the millions.

This audit is one in a series of public-API security writeups middleBrick is publishing as part of our case-study program. We scanned https://rickandmortyapi.com/api/character/1, the canonical 'fetch a character by ID' endpoint that every tutorial uses. The scan returned nine findings, and this writeup walks through them with the same triage framing we use elsewhere: distinguishing 'finding correctly raised, severe in context' from 'finding correctly raised, structural to the API's purpose.' On Rick and Morty API, the split is roughly 1-of-9 (the unpaginated-array pattern) to 8-of-9 (everything else).

02

Threat Model

The threat model for the Rick and Morty API has two distinct readings.

The API itself

The threat surface is essentially zero. There is no real PII, no real credentials, no real state. Every response is read-only canonical data about a fictional show. An attacker compromising the service yields nothing of value; the only meaningful operational risk to the maintainer is denial-of-service, and the project's CDN/cache layer absorbs that comfortably.

The apps that consume it

This is where the audit findings get more interesting. The two CRITICAL findings — 'no authentication' and 'IDOR via sequential IDs' — are propagated patterns. A student building 'a Rick and Morty character browser' as their first React-Query tutorial sees a working pattern: fetch /character/1, render, click 'next', fetch /character/2. They learn that 'ID-based fetching with no auth check is a normal API shape.' When they apply the same pattern to a real authenticated app — fetch /users/1, render, no per-object auth — they ship the IDOR pattern into production. The 12-of-12 schema match the scanner reports is precisely what makes the pattern feel safe; it's the mock confirming that the surface is uniform, which is exactly the property that bites in production.

Cross-cutting risk: the embedded-array pattern

The MEDIUM 'large unpaginated collection' finding flags that the /character/1 response embeds episode as an array of 51 URLs. On Rick and Morty's catalog this is fine — there are a finite, small number of episodes per character and the dataset doesn't grow. On a derived application (any app that uses the same response shape for user-scoped collections), the pattern propagates: the developer learns that 'embedding a related-resource array in the parent response without pagination is a normal API shape.' When the application's user-scoped collections grow to 500, 5000, or 50,000 items per parent, the same pattern becomes a denial-of-wallet vector — every fetch of the parent now drags megabytes of related URLs that the client probably doesn't need. The Rick and Morty API itself is fine; the pattern it teaches is the propagated risk.

03

Methodology

middleBrick ran a black-box scan against https://rickandmortyapi.com/api/character/1. Twelve security checks executed; nine surfaced findings. The scan was read-only — no write methods, no auth headers, no destructive payloads.

The active BOLA probe is what produced the CRITICAL 'confirmed IDOR' finding. middleBrick observed a numeric ID in the URL path (/character/1), incremented it to /character/2, and compared response shapes. Both returned 200, both had identical sets of top-level keys (12 keys: id, name, status, species, type, gender, origin, location, image, episode, url, created), so the schema match is 12/12. The scanner records this as 'enumerable and structurally identical' — the strongest IDOR signal it produces. On a public catalog, this is the API working correctly; on a per-user resource API, it would be the bug.

The 'large unpaginated collection' finding came from response-body parsing. The scanner walks JSON arrays in the response and flags any array longer than 20 items where no pagination metadata is present in the response or in the response Link header. Rick and Morty's episode field is an array of 51 string URLs with no surrounding pagination object. The finding is correctly raised; the severity in context is muted by the array's bounded nature.

The other findings — wildcard CORS, missing rate-limit headers, missing security header (HSTS), DELETE/PUT/PATCH advertised, no URL versioning — are the structural set we see on every public-mock REST API in this case-study series.

04

Results Overview

Rick and Morty API received a B grade with a score of 78. Nine findings: two CRITICAL, two HIGH, two MEDIUM, three LOW.

The two CRITICALs are (1) 'API accessible without authentication' and (2) 'sequential IDs with confirmed IDOR.' On any unauthenticated catalog, both findings will fire and both are correct reads. The IDOR confirmation is the more interesting of the two because the scanner explicitly compared schemas and recorded a 12-of-12 match — the strongest IDOR signal it issues. Whether that signal represents an exploitable bug depends entirely on whether the data behind those IDs is supposed to be readable by anyone or only by the resource owner. On a fictional-character catalog, it's supposed to be readable by anyone.

The two HIGH findings are wildcard CORS (correct for a service called from arbitrary tutorial hosts) and missing rate-limit headers (true — the API doesn't emit X-RateLimit-* even though it does enforce a soft 10,000 req/day limit per IP).

The two MEDIUM findings are the 'large unpaginated collection' (51 episode URLs in /character/1) and 'IDOR risk: numeric object ID in response body.' The second is a deduplicated lower-confidence restatement of the CRITICAL — the scanner's category lens triangulates the same observation from two angles.

The three LOW findings are missing HSTS, DELETE/PUT/PATCH advertised via OPTIONS, and no URL versioning (the path is /api/character/1, not /api/v1/character/1).

05

Detailed Findings

Critical Issues 2
CRITICAL CWE-306

API accessible without authentication

The endpoint returned 200 without any authentication credentials.

Remediation

Implement authentication (API key, OAuth 2.0, or JWT) for all API endpoints.

authentication
CRITICAL CWE-639

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 (12/12 keys identical) — strong IDOR evidence.

Remediation

Use UUIDs or non-sequential identifiers. Implement object-level authorization checks.

bolaAuthorization
High Severity 2
HIGH CWE-942

CORS allows all origins (wildcard *)

Access-Control-Allow-Origin is set to *, allowing any website to make requests.

Remediation

Restrict CORS to specific trusted origins. Avoid wildcard in production.

inputValidation
HIGH CWE-770

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).

Remediation

Implement rate limiting (token bucket, sliding window) and return X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After headers.

resourceConsumption
Medium Severity 2
MEDIUM CWE-639

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.

Remediation

Use UUIDs for object identifiers. Verify per-object authorization on every request. Never expose internal IDs for resources the user doesn't own.

bolaAuthorization
MEDIUM CWE-770

Large unpaginated collection in response

Response contains large arrays: $.episode (51 items). No pagination metadata detected.

Remediation

Use cursor-based pagination. Limit collection size per response.

resourceConsumption
Low Severity 3
LOW CWE-693

Missing security headers (1/4)

Missing: HSTS — protocol downgrade attacks.

Remediation

Add the following headers to all API responses: strict-transport-security.

authentication
LOW CWE-650

Dangerous HTTP methods allowed: DELETE, PUT, PATCH

The server advertises support for methods that can modify or delete resources.

Remediation

Only expose HTTP methods that are actually needed. Disable TRACE, and restrict DELETE/PUT/PATCH.

inputValidation
LOW CWE-1059

No API versioning detected

The API URL doesn't include a version prefix (e.g., /v1/) and no version header is present.

Remediation

Implement API versioning via URL path (/v1/), header (API-Version), or query parameter.

inventoryManagement
06

Attacker Perspective

The attacker has nothing to gain by attacking Rick and Morty API directly. The interesting work is reading the tutorials that use it and mapping the patterns those tutorials teach.

Walk the ID range and read the schema

The first move on any unfamiliar API is to walk the ID range. Against Rick and Morty, 1..826 returns canonical character records and 826+ returns 404. That's the API's documented bound; an attacker is not learning anything new. Against a tutorial-derived app — anything that copied the /character/{id} pattern into its own resource — the same walk reveals the application's user count, the resource creation pattern, and the gaps in the ID space (deleted users, soft-deleted records). All of this is intelligence the attacker can use without ever sending a credential.

Inspect the embedded-URL pattern

The episode array in /character/1 is 51 URLs long. On a real authenticated app that copied this response shape — say, /teams/{id} returning a members: [...] array of URLs — the URLs themselves leak structure: which team has 5 members vs 500, which member URLs are reachable, and which IDs in the URL space are 'live.' The pattern is the bug; the data is the byproduct.

Probe the GraphQL endpoint

Rick and Morty also exposes a GraphQL API at /graphql. We did not scan it in this audit (out of scope for the REST writeup), but on any real GraphQL API, the introspection query is the first probe to run. Whether the Rick and Morty GraphQL endpoint has introspection enabled is something a maintainer can check, and a developer copying the GraphQL pattern into their own app should make sure introspection is disabled on their production deployment.

07

Analysis

The IDOR confirmation is the finding worth reading carefully. middleBrick issued two requests:

GET https://rickandmortyapi.com/api/character/1
→ 200 OK with keys: id, name, status, species, type, gender, origin,
  location, image, episode, url, created

GET https://rickandmortyapi.com/api/character/2
→ 200 OK with the same 12 keys

The scanner records the schema match as 12/12 — every key in the first response appears in the second, with the same types. This is the strongest IDOR signal because it confirms two things: (a) the resources at /character/1 and /character/2 are structurally identical, and (b) there is no per-resource authorization gating access. Together those two facts mean an unauthorized request can enumerate the entire resource set.

On Rick and Morty's catalog, this is the correct behavior. On a per-user resource API — anything where each ID corresponds to data that should be visible only to the resource owner — the same pair of facts would be the bug.

The episode-array finding is more subtle. /character/1's response includes episode as an array of 51 strings, each of which is a fully-qualified URL pointing back to the same API. The full body of the response is approximately 1.8 KB; the episode array alone is roughly 1.4 KB of that. For a single character lookup, this is not a problem. For an application that needs to list all 826 characters, it adds up: a naive implementation that fetches /character?page=1..42 ends up dragging the full episode array for every character on every page, multiplying the wire bytes by roughly 5-10×. The pattern is on the maintainer's roadmap (a sparse-fields GraphQL query is the documented workaround), but it's the kind of pattern that gets copied into derived apps without the optimization.

The CORS wildcard is the standard finding for any service that wants to be called from arbitrary tutorial hosts. The header is Access-Control-Allow-Origin: * without the credentials pair, so it's the HIGH-severity wildcard-only variant.

08

Industry Context

Rick and Morty API sits in the same niche as PokéAPI, SWAPI, and the Studio Ghibli API: 'free public catalog of fictional-canon data for tutorial use.' Compared to those peers, Rick and Morty has the most active community and the cleanest GraphQL implementation. The scoring profile is similar across this niche — the same five or six findings (no auth, IDOR via sequential IDs, wildcard CORS, no rate-limit headers, no versioning, embedded URL arrays) appear on all of them, because the niche has converged on roughly the same response-shape patterns.

For compliance: there is no real PII in the responses, so GDPR / CCPA / HIPAA are not in scope. The maintainer collects standard hosting analytics; that is outside the scope of the API surface itself.

OWASP API Top 10 2023 mapping: API1 (broken object-level authorization) is technically present (the IDOR finding) but structurally — the API has no objects to authorize against. API2 (broken authentication) likewise. API3 (broken object property authorization) does not apply. API6 (unrestricted access to sensitive business flows) does not apply. API8 (security misconfiguration) covers the wildcard CORS and missing rate-limit headers. API9 (improper inventory management) covers the no-versioning finding. API10 (unsafe consumption of APIs) is the lens that fits the embedded-URL-array pattern, although the typical use of API10 is more about consuming third-party APIs than producing them.

09

Remediation Guide

No rate-limit headers

Emit X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After matching the documented per-IP ceiling. Lets clients back off cleanly and reduces the support burden of 'why am I being throttled.'

import rateLimit from 'express-rate-limit';
app.use(rateLimit({
  windowMs: 24 * 60 * 60 * 1000,
  max: 10000,
  standardHeaders: 'draft-7',
  legacyHeaders: false,
  keyGenerator: (req) => req.ip
}));

Large embedded URL array (episode field with 51 entries)

Document the sparse-fields workaround prominently; consider an explicit ?fields=name,species,image query parameter that lets tutorial consumers fetch a slim character object.

// Express, simple sparse-fields handler
app.get('/api/character/:id', async (req, res) => {
  const character = await loadCharacter(req.params.id);
  const fields = req.query.fields?.toString().split(',');
  if (fields) {
    const slim = Object.fromEntries(
      Object.entries(character).filter(([k]) => fields.includes(k))
    );
    return res.json(slim);
  }
  res.json(character);
});

OPTIONS advertises DELETE / PUT / PATCH on a read-only catalog

Restrict the framework's CORS preflight allowed methods to GET / HEAD / OPTIONS only.

import cors from 'cors';
app.use(cors({
  origin: '*',
  methods: ['GET', 'HEAD', 'OPTIONS'],
  allowedHeaders: ['Content-Type']
}));

No HSTS

Add HSTS at the reverse proxy or via Helmet.

import helmet from 'helmet';
app.use(helmet.hsts({
  maxAge: 31536000,
  includeSubDomains: true,
  preload: true
}));

Documenting the 'intentionally public' status

Add a brief security note to the README and the API homepage stating 'all endpoints are intentionally public; no authorization is required by design.' This closes the gap between scanner output and consumer triage.

## Security

All endpoints in this API are **intentionally public**. There is no
authentication or authorization layer. Sequential IDs are an enumerable
catalog (1..826 characters, 1..126 episodes, 1..126 locations) — by design.

If you fork this codebase for a real authenticated application, you must
add per-object authorization before exposing any resource that should
not be world-readable.
10

Defense in Depth

For the maintainer of Rick and Morty API, the defenses are roughly the same as for any public catalog: emit rate-limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After) so clients can back off; add HSTS at max-age=31536000; includeSubDomains; preload; disable the x-powered-by header; document the soft per-IP rate limit prominently. None of these change the API's behavior; they harden the surface for clients that hit it in CI and they make the response carry information that scanners and consumers expect to find.

On the IDOR finding specifically: there is nothing to fix. The whole point of the API is that /character/1 is publicly readable. The defense is not on the API side but in how the API documents itself — a clear note in the API homepage and the OpenAPI spec ('all endpoints are intentionally public; no authorization is required') closes the gap between scanner output and actual risk for downstream consumers.

For consumers — the apps and tutorials that use Rick and Morty — the defenses are: (1) don't copy the 'no auth, sequential IDs' pattern into a real app without adding per-object authorization; (2) don't copy the embedded-URL-array pattern without considering pagination for collections that can grow; (3) when migrating from this API to a real backend, audit the propagated patterns first and add the security controls that the public catalog didn't need.

11

Conclusion

Rick and Morty API received a B (78/100) because every one of the nine findings is correctly raised and eight of them describe a public catalog API working exactly as it should. The remaining one — the embedded 51-URL episode array — is a pattern worth knowing about because of how it propagates into derived apps, not because it's a problem on the catalog itself.

For tutorial consumers: this is one of the cleanest, longest-running, best-maintained public datasets you can teach against. Use it. Just don't carry the 'no auth, sequential IDs, embed everything' shape into a real backend without revisiting each pattern.

For the maintainer: the only optional hardening worth considering is emitting rate-limit headers and adding a 'no authorization is required, by design' note in a place a scanner can find.

Frequently Asked Questions

The scanner says 'confirmed IDOR.' Is the Rick and Morty API insecure?
No. IDOR (Insecure Direct Object Reference) is the term for an API that lets an attacker access resources by guessing or enumerating IDs without authorization. On a public catalog, every resource is supposed to be world-readable, so the 'IDOR' finding is the API correctly reflecting the catalog's intent. The scanner correctly flagged the structural pattern; the severity in context is informational.
Should I use this API in production?
If you're shipping a fan project (a Rick and Morty character browser, a quote generator, a trivia game), absolutely. If you're shipping anything that depends on the API being available with a specific SLA or that needs commercial-grade rate limits, no — the project is a free fan service and depending on it for revenue-critical paths is bad architecture.
What's the rate limit?
Roughly 10,000 requests per day per IP per the project's documentation. The API does not emit X-RateLimit-* response headers, so you have to keep track of your own quota. The standard pattern is to cache responses aggressively client-side (the data is canonical and doesn't change).
What about the GraphQL endpoint at /graphql?
We didn't scan it in this audit — the case study is scoped to the REST surface at /api/character/1. The GraphQL surface has its own security considerations (introspection enabled or not, query complexity limits, depth limits, batching attacks); a follow-up audit could cover those. For the purposes of this writeup, we treat the REST and GraphQL surfaces as separate.
Why does /character/1 return 51 episode URLs in the response?
Because the canonical relationship between Character and Episode is many-to-many, and the API embeds the relationship inline rather than requiring a separate /character/1/episodes call. The pattern is fine for a catalog of bounded size; it's worth knowing about because a similar pattern on a user-scoped API can become a denial-of-wallet vector.