FakeStoreAPI requires auth on HEAD but not GET. That's not a feature.
https://fakestoreapi.com/products/1About This API
FakeStoreAPI is a public, no-auth-required REST API that serves a fixed catalog of fake e-commerce products — twenty items across categories like men's clothing, jewelry, and electronics. Each product has a title, price, description, category, image URL, and a rating block. It was built to be the first backend in a React tutorial. You can fetch('https://fakestoreapi.com/products'), get twenty products back, render them as cards, and you've just built "a store" in a fifteen-minute video.
The API's reach is hard to overstate. The FreeCodeCamp e-commerce tutorial uses it. Every React-Query tutorial with a "data fetching" chapter uses it. A large percentage of "build a shop" YouTube playlists point at it. The Udemy "Complete React Course" sections on data fetching use it. If you ever shipped a Pluralsight, Egghead, Scrimba, or TraversyMedia tutorial involving useEffect-to-fetch product-grid, you've probably had FakeStoreAPI open.
Beyond the product catalog, FakeStoreAPI ships a set of endpoints that make the mock feel more realistic: /auth/login returns a JWT-shaped token, /carts accepts POST/GET/PUT for shopping carts, and /users echoes fake user records. These endpoints are deliberately stateless — POST requests appear to succeed but don't persist. The mock is doing exactly what a mock should do. The problem is that the surface-level behavior is convincing enough that learners sometimes don't realize they're looking at a mock, and carry its patterns into code where persistence, authentication, and authorization matter.
This audit is written for the student or junior developer who built their first e-commerce app against FakeStoreAPI, and who is about to build their second one against a real backend. The findings are mostly not about FakeStoreAPI's safety — they're about what you should unlearn from the experience.
Threat Model
FakeStoreAPI itself is a safe target. The data is synthetic, the endpoints are stateless, no user ever logs in for real, no cart is ever real. The threat model is not about attackers compromising FakeStoreAPI; it's about what patterns propagate into the real e-commerce apps that students build after completing the tutorials.
The propagated cart pattern
The most common pattern taught against FakeStoreAPI is "when the user clicks Add to Cart, POST to /carts." The mock returns 200 OK with a cart ID and a date field. Students see the 200, check it off as "cart works," and ship. What they never test — because the mock never fails — is whether the cart actually persisted. On real e-commerce backends, carts require authentication, idempotency, price-at-time-of-add locking, and inventory reservation. None of these are present in FakeStoreAPI's mock, and a student shipping to production without adding them loses orders silently under load.
The propagated login pattern
FakeStoreAPI's POST /auth/login returns a JWT-shaped string like eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.... Students store it in localStorage and attach it to future requests via Authorization: Bearer <token>. This is the pattern every tutorial teaches. But the mock doesn't verify the JWT on any protected endpoint — it's just a string — so students learn "store token in localStorage" without learning the countermeasures (XSS exfiltration, token rotation, refresh-token handling, sliding expiry) that a real JWT flow requires. The pattern survives the tutorial; the countermeasures don't.
The propagated CORS pattern
Every response has Access-Control-Allow-Origin: *, which is what makes the tutorial work from any localhost or Codesandbox origin. When the student moves to a real backend, they often copy the CORS config from the tutorial into their own Express or FastAPI server. For an authenticated e-commerce API that handles payment intents and user data, wildcard CORS is a browser-reachable attack surface.
The inconsistent auth finding specifically
HEAD returns 401 while GET returns 200 on the same path. This is a genuine implementation quirk — probably a middleware ordering bug. For the audit it means an automated script walking HEAD-to-discover-endpoints gets a different set than one walking GET, which is unusual enough to be worth calling out. On any real API, GET and HEAD should have symmetric authentication because HEAD is definitionally "GET without the body."
Methodology
middleBrick ran a black-box scan against https://fakestoreapi.com/products/1. Ten security checks executed across OWASP API Top 10 categories: authentication, authorization (BOLA/BFLA/property), input validation, CORS, rate limiting, and inventory management. LLM-specific probes did not fire — FakeStoreAPI's fingerprint is plain REST with no inference-related signals.
The multi-method probe is what surfaced the inconsistent-auth finding. middleBrick sent HEAD, GET, and OPTIONS to the same URL. HEAD returned 401 with a WWW-Authenticate challenge; GET returned 200 with the product JSON; OPTIONS returned 204 with an Allow header listing GET, POST, PUT, DELETE, PATCH. The asymmetry is the finding — in a correctly-configured API, HEAD is identical to GET minus the body, so their auth behavior should match.
Active IDOR probing compared /products/1 against /products/2. The response schemas matched 7-of-7 keys (id, title, price, description, category, image, rating), and both returned 200 with different product data. The scanner flagged this as confirmed IDOR via adjacent-ID enumeration, correctly. On a public product catalog this is the intended behavior; the flag is structural rather than actionable.
No destructive methods were issued. No authentication was attempted. The Authorization header was never sent, not even the fake tokens that the documented login flow produces.
Results Overview
FakeStoreAPI pulled a grade of C with a score of 75. Ten findings: two critical, three high, one medium, four low. That distribution is closer to ReqRes's (which had seventeen findings) than to PokéAPI's (twelve, mostly mock-structural) despite FakeStoreAPI being smaller than either.
The two CRITICAL findings are the structural pair — no authentication required on GET, and confirmed IDOR via sequential product IDs. Both are intentional for a public product catalog. The finding we keep flagging across every public-mock audit in this series: your scanner will raise these as CRITICAL, and it's worth having language to explain why they're by design rather than silencing the severity.
The three HIGH findings are the interesting ones. Inconsistent authentication across HTTP methods is the unique-to-FakeStoreAPI quirk — HEAD requires auth, GET doesn't. Wildcard CORS is structural to any public mock. Missing rate-limit headers is structural to any public mock that hasn't invested in the operational layer yet.
The one MEDIUM finding — numeric object ID in the response body — is the same correct-for-a-mock, would-be-BOLA-on-a-real-API signal we've seen before. Four LOWs round it out: missing security headers, DELETE/PUT/PATCH advertised via OPTIONS, no URL versioning, and X-Powered-By: Express.
What's conspicuously absent is any finding on FakeStoreAPI's login flow, cart behavior, or user endpoints. That's because the scanner didn't probe those paths — it hit /products/1, not /auth/login or /carts. A full spec-driven scan would almost certainly find more: the mock's login returns a token without verifying credentials, the cart POST accepts arbitrary bodies, and the user endpoints echo whatever you send. Those weren't in this scan's scope, but they inform the threat-model and remediation sections of this write-up because they are what students consume from the API.
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 (7/7 keys identical) — strong IDOR evidence.
Use UUIDs or non-sequential identifiers. Implement object-level authorization checks.
Inconsistent authentication across HTTP methods
GET returns 200 without auth, but some methods (HEAD) require authentication.
Apply authentication uniformly across all HTTP methods for each endpoint.
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.
Missing security headers (4/4)
Missing: HSTS — protocol downgrade attacks; X-Content-Type-Options — MIME sniffing; X-Frame-Options — clickjacking; Cache-Control — sensitive response caching.
Add the following headers to all API responses: strict-transport-security, x-content-type-options, x-frame-options, cache-control.
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.
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.
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
The attacker approaching FakeStoreAPI has no reason to attack FakeStoreAPI. The service holds no data, no accounts, no state. The attacker's interesting role here is as a diagnostician of downstream code — the real stores built against FakeStoreAPI's patterns.
Read the tutorial, find the pattern
The most productive attack is to read the five most-viewed "build a React e-commerce app" tutorials on YouTube and extract the patterns they teach. You'll end up with a shortlist: localStorage for auth tokens, POST /carts without idempotency keys, no stock or reservation check on checkout, product prices rendered from the API response rather than re-validated server-side on add-to-cart, and CORS wildcard in the backend code. Then search GitHub for repos that match three or more of those patterns simultaneously. The ones with active deployments and real payment integrations are the real targets.
XSS-to-token in tutorial-derived apps
Because the tutorial stores the login token in localStorage, any XSS on a tutorial-derived store steals the token. A student who deployed their FakeStoreAPI-based tutorial to my-shop.vercel.app and then swapped the backend for a real one without revisiting the token storage has built an XSS-to-full-account-takeover chain. The fix is to move tokens to HttpOnly cookies or to a short-lived in-memory pattern with refresh tokens; neither is taught in the canonical tutorials.
Race the cart on a real store
The mock cart endpoint returns immediately with 200. Students who promote this pattern to a real backend frequently skip the idempotency-key check and the price-at-time-of-add lock. An attacker then races the add-to-cart with price changes between check and commit: read current price, add-to-cart, observe that the price in the cart is captured at add-time rather than verified at checkout. On a real store this is free money until the first audit notices the price delta.
Analysis
The inconsistent-auth-across-methods finding is the technically interesting one. On HEAD:
$ curl -I https://fakestoreapi.com/products/1
HTTP/2 401
www-authenticate: Basic realm="Access restricted"On GET:
$ curl https://fakestoreapi.com/products/1
HTTP/2 200
content-type: application/json; charset=utf-8
{"id":1,"title":"Fjallraven - Foldsack No. 1 Backpack...","price":109.95,...}The probable cause is a middleware ordering bug — the Basic-Auth middleware is attached before the path handler in a way that triggers on HEAD but is skipped by the handler's GET short-circuit. This isn't dangerous on FakeStoreAPI (there's nothing to protect anyway), but the asymmetry is a real implementation defect. It also breaks clients that use HEAD to cheaply check for resource existence before issuing a GET — they get a 401 that isn't actually about authorization.
The CORS and rate-limit findings are what we've seen on every spearhead. Access-Control-Allow-Origin: *, no X-RateLimit-*, no Retry-After. Both are correct-for-a-mock and copy-this-pattern-at-your-peril for anyone building a real store.
The sequential-ID / IDOR pair:
GET /products/1 → 200 (Fjallraven Backpack)
GET /products/2 → 200 (Mens Casual Premium Slim Fit T-Shirts)Both return identically-shaped JSON with different product content. On a public catalog this is the whole point. On a catalog gated by user ownership ("show me products I have permission to buy") the same pattern would enable IDOR enumeration of the hidden catalog. The mock doesn't gate, so the finding is structural.
Industry Context
FakeStoreAPI sits in the same taxonomy as JSONPlaceholder and DummyJSON: the public mock whose explicit job is to return plausible-looking data for teaching. Where it differs from both is the domain — e-commerce specifically — and the level of fidelity. FakeStoreAPI is plausible enough that students can build something that looks like a store rather than something that looks like a demo. That's great for teaching and dangerous for propagation.
Compared to real e-commerce backends — Shopify's Storefront API, Commerce.js, Swell — FakeStoreAPI's posture is obviously different and should be. The real backends enforce authentication, carry session state, validate product prices on checkout, enforce stock reservation, apply tax and shipping calculations, and log every transaction. Tutorials using FakeStoreAPI necessarily skip all of these because the mock doesn't support them. A student who learned e-commerce from a FakeStoreAPI tutorial and then deploys a real Shopify integration has an accurate mental model of the front-end flow and zero exposure to the back-end complexity.
Compliance context: actual e-commerce APIs are in scope for PCI-DSS (any handling of card data), GDPR / LGPD / PDPL (any handling of user PII), and often local tax-invoice regulations (in LATAM: SAT Mexico, DGII in Dominican Republic). None of these apply to FakeStoreAPI itself. All of them apply to the real store the student ships next. The gap between tutorial and production is larger than most tutorials acknowledge.
OWASP API Top 10 mapping for what this audit finds: API1 (intentional no-auth), API3 (BOLA via sequential IDs, intentional), API5 (BFLA potential on inconsistent-auth), API8 (security misconfiguration via wildcard CORS and missing rate-limit headers), API9 (versioning and framework disclosure). Five of ten categories touched. What's not touched but matters downstream: API2 (broken authentication in the derived apps using localStorage tokens), API6 (unrestricted access to sensitive flows in cart add/checkout patterns), API4 (unrestricted resource consumption in unpaginated product listings).
Remediation Guide
Inconsistent authentication across HTTP methods
Ensure HEAD and GET on the same path go through the same auth middleware and return consistent status codes. Usually a middleware ordering fix.
// Express: attach auth before the route, not before the method handler
function publicReadOnly(req, res, next) {
// No auth required — applies identically to GET and HEAD
next();
}
app.use('/products', publicReadOnly);
app.get('/products/:id', (req, res) => res.json(getProduct(req.params.id)));
app.head('/products/:id', (req, res) => {
if (!getProduct(req.params.id)) return res.status(404).end();
res.status(200).end();
}); Missing rate-limit headers
Emit X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After using the documented policy.
import rateLimit from 'express-rate-limit';
app.use(rateLimit({
windowMs: 60_000,
max: 200,
standardHeaders: 'draft-7',
legacyHeaders: false
})); Missing security headers
Add HSTS, X-Content-Type-Options, X-Frame-Options, and a sensible Cache-Control via helmet.
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: false, // JSON API
hsts: { maxAge: 31536000, includeSubDomains: true },
noSniff: true,
frameguard: { action: 'deny' }
})); Consumer pattern: don't inherit the localStorage token
For derived real-store apps, store auth tokens in HttpOnly+Secure cookies rather than localStorage. Eliminates the XSS-to-token-theft chain.
// Server sets cookie
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 1000 * 60 * 60 * 24 * 7
}); Consumer pattern: idempotent cart add
Real cart endpoints must accept an idempotency key on POST and detect retries. FakeStoreAPI doesn't need this, real stores do.
app.post('/carts/add', async (req, res) => {
const key = req.get('Idempotency-Key');
if (!key) return res.status(400).json({ error: 'Idempotency-Key required' });
const existing = await kv.get(`idem:${key}`);
if (existing) return res.status(200).json(existing);
const result = await addToCart(req.user.id, req.body);
await kv.put(`idem:${key}`, result, { ttl: 86400 });
res.status(201).json(result);
}); Consumer pattern: server-side price validation on checkout
Never trust client-sent prices on checkout. Re-read product prices from your own database at checkout time and compare against the cart.
app.post('/checkout', async (req, res) => {
const cart = await getCart(req.user.id);
for (const item of cart.items) {
const current = await getProductPrice(item.productId);
if (current !== item.priceAtAdd) {
return res.status(409).json({ error: 'price_changed', productId: item.productId });
}
}
// proceed to payment intent creation
}); X-Powered-By framework disclosure
One line.
app.disable('x-powered-by'); Defense in Depth
If you maintain FakeStoreAPI or similar mock e-commerce APIs, the highest-leverage change is to fix the inconsistent-auth quirk. HEAD and GET should respond the same on the same path — if both are public, 200 on both; if both are protected, 401 on both. This is a middleware fix, not a rearchitecture. Adding rate-limit headers matching your documented policy is the next one. Beyond that, every finding is either structural-to-being-a-mock or cosmetic (suppress X-Powered-By, add X-Content-Type-Options).
A more ambitious defense is to annotate the published behavior. A docs/teaching-scope.md alongside the README that explicitly documents which patterns FakeStoreAPI demonstrates and which patterns students should not inherit would measurably reduce the real-world damage. Something like: "The POST /carts endpoint is stateless and always returns 200. In production, your cart endpoint needs authentication, idempotency keys, inventory reservation, and price-at-time-of-add locking." One paragraph in the right place could save ten thousand broken stores.
If you are a tutorial maker consuming FakeStoreAPI, the defense is to be explicit in your teaching. When you show POST /carts returning 200, narrate what's missing: "in a real backend, this endpoint would also check the user's session, lock the inventory, and return an idempotency token." This is a ten-second addition to a fifteen-minute tutorial and it's the difference between teaching a CRUD pattern and teaching e-commerce.
If you are a student who learned e-commerce from a FakeStoreAPI tutorial, the defense is to assume that every endpoint in your next real store needs more than what the tutorial showed. Authentication checks on every read and write. Idempotency on every write. Server-side price validation on every cart add. Rate-limit headers on every response. A tight CORS allowlist, not a wildcard. These are not optional features; they are what separates a toy from a product.
Conclusion
FakeStoreAPI is a C-grade mock e-commerce API, which for a mock is fine. The findings break down into two categories: structural (correct for a mock, flagged because scanners can't know that), and instructive (patterns the API teaches that shouldn't be inherited into real stores). The one finding that's neither — the HEAD-requires-auth-while-GET-doesn't quirk — is a real bug worth fixing, though low-impact because the resource behind the inconsistency is public anyway.
The real audience for this case study is not the FakeStoreAPI maintainer. It's the student who just finished a React e-commerce tutorial and is about to build a real store. FakeStoreAPI taught you how to render a product grid, how to call POST /carts from a button click, and how to store a returned token in localStorage. It did not teach you authentication, authorization, idempotency, inventory reservation, price validation, CORS allowlists, or rate limiting — because the mock doesn't need any of those. Your real store does.
The specific things to unlearn: that a 200 from POST /carts means the cart persisted, that a token in localStorage is an acceptable auth storage pattern, that wildcard CORS is a reasonable default, that sequential product IDs are fine on a catalog your users have scoped access to, and that HEAD and GET can disagree on authentication. Unlearn those five things and your real store is a step closer to production-safe.