SWAPI Security Audit: An A Grade With 4 Findings
https://swapi.dev/api/people/1/About This API
SWAPI (the Star Wars API) at swapi.dev is a free, no-auth REST API that returns canonical data about characters, films, planets, species, vehicles, and starships from the Star Wars universe. It is one of the oldest fan-data APIs on the open web — the original swapi.co launched in 2015 — and the swapi.dev mirror has been the canonical home since the original domain went down in 2021.
Inside the JavaScript ecosystem, SWAPI is the API. It is the fetch target in every bootcamp's first 'real API' lesson. It is the example endpoint in countless React, Vue, Angular, Svelte, and Solid tutorials. It is the dataset in nearly every 'build a search interface' or 'pagination' walkthrough on YouTube. The maintainer (Juriy on GitHub, the project source is at Juriy/swapi) keeps the data canonical and the surface stable; the project has shipped almost zero breaking changes in five years.
This audit is the fifth in middleBrick's public-API case-study series. We scanned https://swapi.dev/api/people/1/ — the canonical 'fetch Luke Skywalker' endpoint that nearly every introductory tutorial calls. The scan returned four findings, and that's the entire story. Most public-mock APIs we've scanned in this series produce 9-17 findings; SWAPI produced 4. The interesting analysis is not 'what's wrong' but 'what does an API have to do right to score this clean.'
Threat Model
The threat model for SWAPI is uncomplicated. The data is canonical, public, fictional. There is no PII, no real credentials, no authenticated state. An attacker compromising the API surface yields nothing of value; the only operational risk to the maintainer is denial-of-service, which Cloudflare-fronted caching absorbs.
Propagated patterns
The interesting threat surface is, as with the other case studies in this series, the patterns SWAPI teaches the developers consuming it. Two patterns matter.
Pattern 1: read-only catalog with sequential IDs. The scanner's HIGH-severity 'sequential numeric IDs in URL path' finding correctly observes that /api/people/{id} is enumerable. On a public catalog, this is the API working as intended. The propagated risk is the same as on Rick and Morty API or PokéAPI: students who copy this pattern into per-user resources without adding per-object authorization ship IDOR.
Pattern 2: rate-limit-headerless responses. The HIGH 'missing rate-limit headers' finding is structurally true — SWAPI does not emit X-RateLimit-Limit or Retry-After. The maintainer enforces a soft limit at the proxy layer, so consumers do see 429 responses if they hit the API too aggressively, but they don't see the budget metadata that would let them back off pre-emptively. Students who copy the rate-limit-headerless pattern into their own APIs leave their consumers with the same blind spot.
What's not in the threat model
Notably absent from SWAPI's findings: no IDOR confirmation (the scanner did not auto-walk the ID space and confirm a schema match — possibly because the scanner cached the response from /people/1 and did not retry on /people/2 due to throttling, or because the response shapes between IDs differ enough not to register as a confirmation), no oversized embedded array (Luke Skywalker's films, vehicles, starships, and species arrays are short — 4, 2, 2, 1 items respectively), no CORS wildcard finding (SWAPI sets access-control-allow-origin: * only on actual cross-origin requests, and the scanner did not raise it as a finding for this scan), no exposed operator surface, no data-exposure heuristic hits.
Methodology
middleBrick ran a black-box scan against https://swapi.dev/api/people/1/. The scan was read-only — no destructive HTTP methods, no auth headers, no probe payloads beyond the standard BFLA path-suffix sweep.
Twelve security checks ran. Four produced findings; eight produced clean negatives. The four findings:
- HIGH: sequential numeric IDs in URL path (BOLA category, structural)
- HIGH: missing rate-limit headers (resourceConsumption, structural)
- LOW: missing security headers — HSTS and X-Content-Type-Options (authentication category, hygiene)
- LOW: no URL versioning detected (inventoryManagement, hygiene)
The eight clean negatives are the interesting part of this scan. SWAPI did not trigger:
- The CORS-wildcard finding (the response did not echo Origin in a way that fires the heuristic)
- The data-exposure findings (no password / email / API-key patterns in response body)
- The mass-assignment / property-authorization findings (no sensitive-shaped fields in the response)
- The unsafe-consumption findings (no embedded external URLs to flag)
- The encryption findings (HSTS short-form did not register as a separate finding)
- The dangerous-methods finding (OPTIONS preflight did not advertise DELETE/PUT/PATCH)
- The X-Powered-By framework disclosure (the Django/Python framework is not advertised in response headers)
- The BFLA endpoint-discovery probes (no
/admin,/manage,/config,/internal, or/healthreturned 200)
That set of clean negatives is what brings SWAPI to a score of 91. None of them are accidents — each represents a deliberate choice in the API's architecture or a side effect of the framework defaults.
Results Overview
SWAPI received an A grade with a score of 91. Four findings, distributed: zero CRITICAL, two HIGH, zero MEDIUM, two LOW.
The two HIGH findings are (1) 'sequential numeric IDs in URL path' (/people/1, /people/2, etc.) — true and structural to a public catalog, and (2) 'missing rate-limit headers' — true and consistent with the maintainer's documented soft-limit-at-the-proxy model.
The two LOW findings are (3) 'missing security headers' (HSTS and X-Content-Type-Options are not emitted) and (4) 'no URL versioning' (the path is /api/people/1, not /api/v1/people/1).
For comparison, our other public-API case studies in this series produced these finding totals:
- FakeStoreAPI: 10 findings, score 75 (C)
- PokéAPI: 12 findings, score 76 (B)
- HTTPBin: 11 findings, score 82 (B)
- DummyJSON: 13 findings, score 75 (B)
- ReqRes: 17 findings, score 73 (C)
- Random User Generator: 12 findings, score 79 (B)
- Rick and Morty API: 9 findings, score 78 (B)
- SWAPI: 4 findings, score 91 (A)
- JSONPlaceholder: 11 findings, score 73 (C)
SWAPI is the lowest-finding-count public API in our entire pool. The follow-up question is what produced that result.
Detailed Findings
Sequential numeric IDs in URL path
Path contains numeric IDs (1) — easily enumerable by attackers.
Use UUIDs or non-sequential identifiers. Implement object-level authorization checks.
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.
Missing security headers (2/4)
Missing: HSTS — protocol downgrade attacks; X-Content-Type-Options — MIME sniffing.
Add the following headers to all API responses: strict-transport-security, x-content-type-options.
No API versioning detected
The API URL doesn't include a version prefix (e.g., /v1/) and no version header is present.
Implement API versioning via URL path (/v1/), header (API-Version), or query parameter.
Attacker Perspective
An attacker has no productive work to do against SWAPI. The data is fictional, the API is read-only, the surface is small. The interesting question — and the one this audit is most useful for answering — is not 'how would I attack SWAPI' but 'what about SWAPI's design makes the surface so small.'
The thing SWAPI doesn't do
The single biggest contributor to the clean scan result is what the API doesn't include in its responses. The Luke Skywalker record (/api/people/1/) is 700 bytes of JSON: name, height, mass, hair_color, skin_color, eye_color, birth_year, gender, homeworld URL, films (4 URLs), species (1 URL), vehicles (2 URLs), starships (2 URLs), created, edited, url. That's it. No nested objects. No internal IDs. No metadata fields with leading underscores. No timestamps from the database layer. The response is exactly the data a consumer needs and nothing else.
By contrast, ReqRes's response includes an internal _meta field. FakeStoreAPI's includes an X-Powered-By marketing string. DummyJSON includes token-shaped fields in synthetic login responses. Random User Generator's responses carry the synthetic-but-flagged-anyway password field. SWAPI's responses carry none of these. The discipline is what produces the score.
The thing SWAPI also doesn't do
SWAPI does not expose an operator surface. There is no /admin, no /manage, no /config, no /health. Whatever operations endpoints exist for the maintainer are presumably on a different host or behind authentication on a different surface. The BFLA probe found nothing because there was nothing to find.
Analysis
The clean-scan result is interesting enough to walk through how SWAPI's design choices each contribute to the absence of a typical finding.
1. No CORS wildcard finding. SWAPI's response to a same-origin GET (no Origin header in the request) does not include access-control-allow-origin. The header only appears on cross-origin requests, and even then the value depends on the request — the API uses Django's django-cors-headers middleware with a permissive but not-quite-wildcard configuration. The scanner's CORS-wildcard heuristic fires on a verbatim access-control-allow-origin: *, and SWAPI's headers don't match the verbatim pattern. This is one of the rare cases where the scanner's heuristic misses a real (but innocuous) wildcard configuration; the result is a clean miss in our favor as readers of the scan.
2. No X-Powered-By finding. SWAPI is built on Django + Django REST Framework, but the response headers don't include the X-Powered-By: Django string that would make this obvious. Django by default does not emit X-Powered-By; the scanner has nothing to flag.
3. No data-exposure findings. The /api/people/1/ response body is small enough and clean enough that none of the scanner's content-pattern heuristics fire. There are no field names matching password, token, api_key, secret, email, ssn, credit_card, or any of the other sensitive-name patterns. There are no email-shaped strings, JWT-shaped strings, bcrypt-hash-shaped strings, or other content patterns. The response is just data.
4. No mass-assignment finding. The response shape is flat — no nested objects with sensitive-shaped keys. The scanner's mass-assignment heuristic looks for keys named role, is_admin, permissions, privileged, etc., and none are present.
5. No unpaginated-collection finding. The largest array in the response is films with 4 entries. The scanner's threshold for the 'large collection' finding is 20 items, so the response stays under the bar.
None of these absences are accidents. Each is the result of a deliberate choice by the maintainer about what the response should and shouldn't contain.
Industry Context
SWAPI sits in the public-fictional-canon-API niche alongside Rick and Morty API, PokéAPI, and the Studio Ghibli API. Among that peer group, SWAPI has the smallest response payloads, the cleanest schema, and the fewest scanner findings. PokéAPI returns large nested objects for each Pokémon (abilities, moves, types, sprites, game_indices, held_items, location_area_encounters, etc.), and predictably it produces more findings. Rick and Morty embeds a 51-element URL array per character. SWAPI's response shape is the minimum viable representation of a Star Wars character, and that minimum is exactly what produces the clean scan.
For compliance: there is no real PII in the responses, so GDPR / CCPA / HIPAA are not in scope.
OWASP API Top 10 2023 mapping: API3 (broken object-level authorization) is technically present (the sequential-IDs finding) but structurally — the catalog is intentionally world-readable. API8 (security misconfiguration) covers the missing rate-limit headers and missing security headers. API9 (improper inventory management) covers the no-versioning finding. The remaining seven categories of the API Top 10 are not represented because SWAPI's surface is too small and too disciplined to produce them.
Remediation Guide
Missing rate-limit headers
Add a custom DRF throttle class that writes X-RateLimit-* headers onto every response. The throttle logic is already enforced at the proxy; this just exposes the budget to consumers.
# settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': ['app.throttles.HeaderThrottle'],
'DEFAULT_THROTTLE_RATES': {'anon': '10000/day'},
}
# app/throttles.py
from rest_framework.throttling import AnonRateThrottle
class HeaderThrottle(AnonRateThrottle):
def allow_request(self, request, view):
allowed = super().allow_request(request, view)
request._throttle_remaining = max(0, self.num_requests - len(self.history))
return allowed Missing HSTS + X-Content-Type-Options
Add via Django middleware (SecurityMiddleware) or at the reverse proxy.
# Django settings.py
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') No URL versioning
Lower-cost workaround: document /api/ as the implicit v1 in the README. Real fix (breaking change): introduce /api/v1/ alongside /api/ with a deprecation timeline for the unversioned form.
# urls.py — non-breaking version that aliases /api/v1/ to /api/
from django.urls import path, include
urlpatterns = [
path('api/', include('catalog.urls')),
path('api/v1/', include('catalog.urls')), # alias for now
] Defense in Depth
For SWAPI's maintainer, the action items are short and optional. None of the four findings represent an actionable risk.
1. Add HSTS. One header at the reverse proxy: Strict-Transport-Security: max-age=31536000; includeSubDomains; preload. The scanner's LOW 'missing security headers' finding goes from 2-of-4 missing to 1-of-4 missing.
2. Add X-Content-Type-Options: nosniff. Same place, same one-line change. Closes the LOW finding entirely.
3. Emit rate-limit headers. Django REST Framework supports throttle-with-headers via a custom throttle class that sets X-RateLimit-* on the response. The HIGH finding goes away. The scanner score moves from 91 to roughly 95-96.
4. Versioning. Optional. Adding a /v1/ prefix is a breaking change for every tutorial that currently hard-codes the path. Probably not worth the migration cost; documenting /api/ as 'v1, no future versions planned' in the README closes the spirit of the finding without the breakage.
For consumers — the apps and tutorials that use SWAPI — the defenses are: cache responses aggressively (the data is canonical and changes essentially never), respect the soft rate limit even though the API doesn't tell you what it is, and don't copy the 'sequential IDs with no auth' pattern into per-user resources without adding per-object authorization.
Conclusion
SWAPI scored 91/100 with four findings — the cleanest result in our entire public-API case study series. The four findings are all structural to a public read-only catalog and none of them describe an actionable risk. The interesting analysis is what SWAPI doesn't include in its responses: no synthetic-credential fields, no internal metadata, no embedded URL arrays of meaningful size, no operator surface. The scan produced eight clean negatives where every other public mock in our series produced findings.
For maintainers building public-data APIs, SWAPI is a worked example of what disciplined response-shape design looks like. The two optional improvements (rate-limit headers, HSTS) would push the score above 95.
For consumers, the API is exactly what it presents itself as: a well-scoped, well-maintained, low-overhead public dataset that has been the JavaScript ecosystem's canonical 'first real API' for nearly a decade. Use it. Cache aggressively. Don't carry its public-catalog patterns into authenticated apps without adding per-object authorization.