DummyJSON teaches JWT auth. Its CORS config undoes every lesson.
https://dummyjson.com/products/1About This API
DummyJSON is a public mock REST API that serves realistic-looking data across several domains: products (with ratings, stock, shipping info, meta blocks), users (with names, emails, addresses, company, phone, bank, crypto, and a hash field), posts, comments, todos, and a full auth flow at /auth/login that returns a JWT. It's the most feature-complete teaching mock currently in wide use. Where JSONPlaceholder returns three fields per object and FakeStoreAPI returns seven, DummyJSON returns twenty-two — enough to render a UI that looks like a real admin dashboard instead of a demo.
The API's audience is beginner-to-intermediate developers learning full-stack patterns. A typical consumer is a bootcamp student in week six working on their first full-CRUD assignment, a YouTube tutorial maker walking through a React-Query + TanStack-Router build, or a self-taught developer building a portfolio project that demonstrates auth, protected routes, and role-based access. The documentation pages walk through login, token usage, protected endpoints, refresh-token rotation, and cart management — effectively a mini-curriculum on how to wire auth into a modern web app.
The design philosophy behind DummyJSON is to return data realistic enough to stress-test UI patterns without exposing real PII. The fake users have fake emails ([email protected]), fake phone numbers, fake crypto addresses. Scanners see the shapes — email-ish patterns, phone-ish patterns, address-ish patterns — and flag them as PII exposure. The shapes are synthetic; the scanner can't tell. That's a false-positive surface for the audit. The real concern, this write-up argues, is that the patterns DummyJSON demonstrates — including a CORS configuration that is genuinely broken — propagate into real applications that the students build after finishing the tutorial.
This case study is written for bootcamp students and tutorial consumers who are likely already building against DummyJSON. It's not arguing you should stop using the service; it's arguing you should know what the service is modeling correctly and where it's teaching a pattern that doesn't generalize.
Threat Model
DummyJSON itself is a safe target. The data is synthetic, the authentication is unverified on any protected path (you can login with any user in the fake database, and the returned JWT isn't rechecked), and there's no real state to corrupt. The threat model is entirely about what the service demonstrates to its consumers.
The propagated CORS pattern
DummyJSON's response to a request with a specific Origin header is to reflect that origin into Access-Control-Allow-Origin and keep Access-Control-Allow-Credentials: true. This is the same pattern we flagged on HTTPBin, and it's the pattern that browsers accept for credentialed cross-origin reads. On DummyJSON there is nothing to read — no real session, no real cookie. On any real application a student copies this pattern into, the CORS configuration is a full cross-origin-read surface for any site the user visits.
The propagated JWT pattern
DummyJSON's /auth/login flow returns a JWT in the response body along with a refresh token. The tutorial teaches the student to store the JWT in localStorage and attach it via Authorization: Bearer <token>. This pattern works on the mock but does not prepare the student for the production version. On a real app, storing a JWT in localStorage is vulnerable to XSS exfiltration; the safer patterns (HttpOnly cookies, short-lived tokens with refresh rotation, in-memory storage) require server support that the mock doesn't demonstrate. Students finish the tutorial thinking they understand JWT auth and have actually learned only the happy-path shape, not the security properties.
The propagated shippingInformation pattern
DummyJSON's user objects include a shippingInformation field containing full address data: name, phone, address1, address2, city, state, postalCode, country. The field is writable via PUT/PATCH per the documented CRUD surface. A student who copies this schema into a real e-commerce backend inherits a mass-assignment risk: if the backend naively passes the request body to User.update(id, body), an attacker can overwrite another user's shipping address by including shippingInformation in a PATCH body. On DummyJSON this is a free-for-all (it's a mock); on a real store it's an account-takeover pathway for shipping-address-based refund scams.
The real-looking PII flag
Emails in the response body trigger the scanner's PII detector. DummyJSON's emails are synthetic — all end in @x.dummyjson.com — so the finding is a false positive for the mock. But any real app whose users share a similar domain suffix (@example-corp.com) and whose responses return unredacted user lists will trigger the same finding on the same logic. The downstream lesson worth teaching is that API responses should scope what's returned to what the caller needs — user lookup by ID returns name and avatar, not the full record including email, phone, and shipping address.
Methodology
middleBrick ran a black-box scan against https://dummyjson.com/products/1. Eleven security checks executed across OWASP API Top 10 categories. LLM adversarial probes did not fire — DummyJSON is a plain REST API with no inference-related signals.
The CORS probes sent three variants of the OPTIONS preflight: no Origin, a benign Origin, and a crafted malicious Origin (https://evil.example). The malicious-Origin probe received back Access-Control-Allow-Origin: https://evil.example with Access-Control-Allow-Credentials: true. That reflection-plus-credentials combination is the CRITICAL finding, and it is the same pattern we previously flagged on HTTPBin.
The active BOLA probe compared /products/1 with /products/2. The schemas matched across all 22 keys (id, title, description, category, price, discountPercentage, rating, stock, tags, brand, sku, weight, dimensions, warrantyInformation, shippingInformation, availabilityStatus, reviews, returnPolicy, minimumOrderQuantity, meta, images, thumbnail). Both returned 200 with different product data. The scanner flagged this as confirmed IDOR, structurally correct for a public catalog mock.
The data-exposure probe searched the response body for PII-shaped patterns. It flagged email addresses (from nested reviews[].reviewerEmail fields) and the shippingInformation block as sensitive-property patterns. Both findings are what a scanner does when it sees the shapes; they are structurally correct and contextually false-positive on the mock itself.
No authentication was attempted. No destructive methods were issued. The scanner did not touch the auth flow, the user endpoints, or the carts path — those remain open questions for a deeper audit.
Results Overview
DummyJSON earned a grade of C with a score of 75 and eleven findings. Three CRITICAL, one HIGH, three MEDIUM, four LOW. The distribution is similar to FakeStoreAPI's but the specific CRITICALs are worse: FakeStoreAPI's CORS was wildcard-only, DummyJSON's is the spec-invalid reflection-with-credentials variant that browsers actually honor for credentialed reads.
The three CRITICAL findings: missing authentication (structural, intentional for a public mock), sequential-ID IDOR (structural, intentional), and CORS origin reflection with credentials (genuine, not structural, same pattern as HTTPBin). The CORS one is the finding to take seriously. On DummyJSON there is no state to protect and no real authentication to hijack; on any real app that copies DummyJSON's middleware configuration, the CORS setting is a full credentialed-cross-origin-read attack surface.
The one HIGH finding is "sensitive fields exposed in response" — specifically the shippingInformation property. The scanner reads the field name as an indicator that the object model contains shipping data, which in turn is a mass-assignment-and-PII concern on real applications. On the mock, it's deliberately populated with fake data for teaching; the risk is in what downstream applications inherit.
The three MEDIUM findings are structural-for-a-mock or pattern-shaped: numeric ID in response body (BOLA signal), shippingInformation indicating a mass-assignment-writable field, and email addresses in the response (the fake-but-real-shaped synthetic emails in the reviews block). Each is correct per the scanner's heuristics and false-positive in the mock's context.
The four LOW findings are hygiene issues: no authentication required on any HTTP method (reiteration of the CRITICAL), DELETE/PUT/PATCH advertised via OPTIONS, HSTS max-age below the recommended 1-year baseline, and no URL versioning.
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 (22/22 keys identical) — strong IDOR evidence.
Use UUIDs or non-sequential identifiers. Implement object-level authorization checks.
CORS reflects arbitrary origin with credentials
The server reflects any Origin header in Access-Control-Allow-Origin AND allows credentials — full cross-origin attack surface.
Validate the Origin header against an allowlist. Never reflect arbitrary origins with credentials enabled.
Sensitive fields exposed in response
Fields found: shippingInformation
Remove sensitive fields from API responses or implement field-level filtering.
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.
Exposure of sensitive object properties (shippingInformation)
Fields shippingInformation in the response indicate the object model includes sensitive properties that could be writable via PUT/PATCH (mass assignment).
Implement allowlists for writable fields. Never accept role, permission, or admin fields from client input. Use separate input/output DTOs.
Email addresses exposed in response body
Response body contains email addresses. 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.
No authentication on any HTTP method
The endpoint returns success for GET and 4 other HTTP methods without authentication.
Implement authentication for all API endpoints. Consider OAuth 2.0, API keys, or JWT bearer tokens.
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.
HSTS max-age is too short
HSTS max-age is 15552000 seconds (recommended: 31536000 / 1 year).
Set Strict-Transport-Security max-age to at least 31536000 (1 year).
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 attacking DummyJSON itself gets nothing — the data is synthetic and the auth is unverified. The interesting attacker perspective is the one that reads DummyJSON as a curriculum and attacks the real applications that come out of its graduates.
Searching for the CORS pattern in production
The CORS configuration DummyJSON ships — reflect arbitrary Origin, allow credentials — is distinctive enough to fingerprint. A GitHub code search for Access-Control-Allow-Origin set to req.headers.origin paired with Access-Control-Allow-Credentials: true returns a long list of real applications that inherited the pattern. Many are small SaaS apps, portfolio projects, and internal dashboards. A subset of those are real targets with real session cookies, and the CORS configuration makes any site the user visits able to read authenticated responses.
Targeting tutorial-derived apps via XSS to JWT
Because DummyJSON tutorials uniformly store the returned JWT in localStorage, any XSS on a tutorial-derived app steals the token. DOM-based XSS via unescaped product names (DummyJSON's titles include characters that rendered naively are HTML-unsafe) or review text is a realistic vector. The attacker who finds a tutorial-derived shop with an XSS then finds the JWT in localStorage.accessToken and has full account access to however the real backend uses that token.
Exploiting inherited shippingInformation schema
A student who inherits DummyJSON's user schema into a real backend and whose update endpoint passes the request body to an ORM without filtering can have their users' shipping addresses overwritten by an attacker. This is a refund-scam precursor: change the victim's shipping address, place an order the victim has already prepaid, intercept the shipment. The pattern is small but real, and it shows up in early-stage e-commerce deployments often enough that PCI-DSS assessors have started asking about mass assignment specifically.
Analysis
The CORS configuration is the finding with the most substantive impact. A request to https://dummyjson.com/products/1 with Origin: https://evil.example returns these headers:
HTTP/2 200
access-control-allow-origin: https://evil.example
access-control-allow-credentials: true
access-control-expose-headers: Set-CookieThis is the browser-honored combination. A malicious site can issue fetch('https://dummyjson.com/users/1', { credentials: 'include' }) and — if the user's browser has any cookies for dummyjson.com — those cookies are sent with the request and the response is returned to the malicious site. DummyJSON doesn't set cookies, so the practical blast radius on the mock is near zero. But the middleware configuration is the one that will ship with the student's real app if they copy DummyJSON's Express or Fastify setup verbatim.
The shippingInformation field in product responses is a more instructive finding. The response for /products/1 includes:
"shippingInformation": "Ships in 1 month"That's a descriptive string, not an address block, so the scanner's heuristic is slightly off on this specific response. But the same field name on user objects (/users/1) returns a full address block:
"address": {
"address": "1745 T Street Southeast",
"city": "Washington",
"state": "DC",
"stateCode": "DC",
"postalCode": "20020",
"country": "United States",
"coordinates": { "lat": 38.867033, "lng": -76.979235 }
}The field shape is the finding. A real API that returns user-address blocks in every user-lookup response (rather than scoping them to self-lookup or explicit fetch-my-address endpoints) is the pattern worth noticing. On DummyJSON the data is synthetic; on a real app the pattern leaks PII to any caller who can enumerate user IDs.
The JWT flow behaves as expected. POST to /auth/login with the username/password of any user in the fake database returns:
{ "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", ... }The tokens are JWT-shaped and decode into plausible payloads (iss, sub, exp, iat). The mock does not verify them on subsequent protected requests — any valid-shaped bearer token is accepted. This is reasonable for the mock's purpose (teaching the happy-path request-response shape) but it means students never experience the failure modes a real JWT flow must handle: token expiry, signature mismatch, algorithm confusion (alg: none), key rotation, refresh-token reuse, and replay attacks within the TTL window.
Industry Context
DummyJSON is the most complete teaching mock in current use. Compared to JSONPlaceholder (strictly read-only, three-field objects), FakeStoreAPI (product-focused, mid-fidelity), or ReqRes (hybrid mock/real), DummyJSON covers the widest range of full-stack patterns. That comprehensiveness is why bootcamps favor it — one API gives you products, users, auth, carts, posts, and comments, so a single mock can drive an entire sixteen-week curriculum.
The trade-off of comprehensiveness is that DummyJSON models more surface area, and more surface area means more scanner findings and more patterns that can propagate. JSONPlaceholder's twelve findings are almost all structural-mock-noise; DummyJSON's eleven findings include one (the CORS reflection) that is a real misconfiguration worth fixing on the service and a real anti-pattern worth not inheriting on downstream apps.
Compliance context on the downstream applications matters here. Tutorial-derived apps that copy the shippingInformation-in-every-response pattern and then add real user data fall into PCI-DSS scope (if payment data is attached), GDPR/LGPD scope (if any identifiable user information is stored), and in LATAM specifically the Open Banking / data-protection frameworks of Brazil (LGPD), Mexico (LFPDPPP), and Colombia (Ley 1581). None of these apply to DummyJSON itself; all of them apply to the student's first real app.
OWASP API Top 10 coverage on this audit: API1 (intentional no-auth), API3 (structural BOLA), API5 (shippingInformation exposure as BOPLA / BOLA-at-property-level), API8 (CORS misconfiguration — the CRITICAL finding), API9 (versioning and HSTS hygiene). Five of ten. What's not flagged but matters downstream: API2 (broken authentication in the derived apps using localStorage tokens) and API6 (unrestricted access to sensitive flows in mass-assignment-shaped update endpoints).
Remediation Guide
CORS reflects arbitrary origin with credentials
Replace the reflection middleware with an explicit allowlist of trusted origins, or set credentials: false on the public mock surface. This is the single most valuable change to DummyJSON because it fixes the pattern students inherit.
// Express with allowlist
import cors from 'cors';
const ALLOWED = new Set([
'https://codesandbox.io',
'https://stackblitz.com',
'https://*.codesandbox.io'
]);
app.use(cors({
origin(origin, cb) {
if (!origin || ALLOWED.has(origin)) cb(null, origin || false);
else cb(null, false); // reject cleanly
},
credentials: false // flip to true only on specific allowlisted paths
})); Mass-assignment risk on shippingInformation
Define an allowlist of permitted update fields in the update schema. additionalProperties: false + named properties is the canonical shape.
components:
schemas:
UserUpdateRequest:
type: object
additionalProperties: false
properties:
firstName: { type: string, maxLength: 100 }
lastName: { type: string, maxLength: 100 }
email: { type: string, format: email, maxLength: 200 }
# shippingInformation intentionally excluded — updates must hit /addresses User-lookup endpoint returns full address block
Scope response fields to the caller. Self-lookup returns full; peer lookup returns a narrow subset.
app.get('/users/:id', (req, res) => {
const user = getUser(req.params.id);
if (!user) return res.sendStatus(404);
if (req.user.id === user.id) return res.json(user); // self
// peer lookup: narrow projection
res.json({
id: user.id,
firstName: user.firstName,
avatarUrl: user.avatarUrl
});
}); Consumer pattern: don't inherit localStorage token
Use HttpOnly cookies on the real backend. Tokens never reach the browser, XSS can't exfiltrate them.
// Server sets cookie instead of returning a token
res.cookie('session', signedSessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 1000 * 60 * 60 * 24 * 7
}); Consumer pattern: JWT verification on protected routes
Real protected endpoints must verify the JWT signature, expiry, issuer, and audience on every request. DummyJSON's mock skips this; your backend cannot.
import { jwtVerify } from 'jose';
const ISSUER = 'https://auth.example.com';
const AUDIENCE = 'https://api.example.com';
async function verify(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.sendStatus(401);
try {
const { payload } = await jwtVerify(token, getPublicKey(), {
issuer: ISSUER,
audience: AUDIENCE
});
req.user = payload;
next();
} catch {
res.sendStatus(401);
}
} HSTS max-age below recommendation
Bump to 31536000 seconds (1 year). One-line change.
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); Defense in Depth
On DummyJSON's side, the single most valuable change is to fix the CORS configuration. Reflecting arbitrary origins with credentials is a real security bug even on a mock — not because of DummyJSON's own exposure but because the configuration is what students will inherit. Replace the reflected-origin middleware with an explicit allowlist (or a documented "credentials: false for the public mock" choice) and the CRITICAL finding disappears, and the pattern students inherit is a safer one.
Publishing a short "inheritance guide" alongside the API documentation would compound that improvement. Three paragraphs are enough: "DummyJSON's login endpoint does not verify the returned JWT on subsequent requests — that's part of being a mock. Your real backend must verify every JWT's signature, expiry, issuer, and audience." "The shippingInformation block is returned on every user response for teaching; your real API should scope address data to self-lookup endpoints only." "The CORS configuration here is intentionally permissive for tutorial origins; your real backend should allowlist specific origins." Those three notes, repeated in ten thousand tutorials, would measurably change the baseline security of tutorial-derived apps over the next few years.
On the consumer side, the defense is to treat DummyJSON as a request-response shape demonstrator rather than an implementation reference. Build your UI against the shape, but when you move to a real backend, don't copy the server's CORS, JWT-verification, or user-response-scoping behavior from DummyJSON. Ask: "what does my real API need to do that DummyJSON doesn't?" and build that. The answers are well-documented elsewhere (Auth0, Supabase, and Clerk all publish solid JWT-auth guides); the tutorial sequence from DummyJSON to a real production app is the seam where students most often carry the wrong patterns across.
A specific pattern worth teaching: replace localStorage.accessToken with a cookie-based flow. Set-Cookie: session=<id>; HttpOnly; Secure; SameSite=Lax on the server, no tokens in the browser at all. XSS can no longer steal the session. This pattern isn't taught in most DummyJSON tutorials because the mock doesn't set cookies, but it's the right pattern for a real app and the switch is small enough that it belongs in the tutorial's "moving to production" chapter.
Conclusion
DummyJSON is a C-grade mock API with eleven findings, which for the level of surface area the service exposes is a reasonable result. Two of the three CRITICALs are structural-and-intended (missing auth, sequential-ID IDOR on a public mock). The third — CORS origin reflection with credentials — is a real misconfiguration that matters more because of what it teaches than because of what it exposes on the mock itself. The same pattern on a real application with session cookies is a browser-reachable credentialed-read attack surface.
For the bootcamp student or tutorial consumer, the useful takeaway is to build your UI against DummyJSON's response shapes and then deliberately unlearn DummyJSON's server-side patterns when you ship your own backend. Three things to unlearn: the reflected-origin-with-credentials CORS middleware, the localStorage-stored JWT without a refresh-rotation strategy, and the user-lookup endpoint that returns full PII blocks regardless of caller.
For the DummyJSON maintainers, the one change with the highest ratio of consumer impact to maintainer effort is fixing the CORS configuration. The mock will still work for every tutorial it currently supports, the CRITICAL finding disappears from every scan against the service, and the pattern students inherit improves from "this is broken" to "this is safer-than-most-mocks." Everything else — the JWT-verification teaching gap, the user-response-scoping pattern — is better addressed with documentation than with code changes, and documentation is what consumers actually need.