Break-in story · TimeBack
One learner can read another learner's report card
The page that shows a student's progress trusts whatever ID is in the web address. Change the number and it hands back someone else's record — no extra permission needed. I proved it with two of our own test learners, then stopped.
Severity
High
At risk
Every learner's progress & grades
Reachable
Public internet, after login
Attacker needs
Any one valid account
?Why it matters
A parent, a curious student, or anyone who signs up for a single free account could page through every other learner's report card by just incrementing a number. That's a privacy breach of minors' education records — the kind that triggers FERPA obligations and breaks trust with every family on the platform. No alarm would sound today, because to the server each request looks like a normal, logged-in user asking for a page.
1The break-in, step by step
1
Sign in as anybody
I logged in as our test learner
swarm_learner_a — an ordinary, lowest-privilege account. No admin, no special role.2
Notice my own ID in the URL
Opening my report card, the address read
/api/v2/learners/48210/report. The 48210 is just my account number, sitting in plain sight.3
Change the number
I swapped in the ID of our second test learner,
swarm_learner_b (48211) — a separate account I also own. The server never checked whether the two were related.4
Receive data that isn't mine
It returned learner B's full report — grades, mastery, attendance. I confirmed the ID belonged to the other test account, captured the response as proof, and went no further.
✓The proof — replayable, non-destructive
# Signed in as swarm_learner_a (our own test learner, id 48210). # Requesting a DIFFERENT owned test learner's report (id 48211). GET /api/v2/learners/48211/report Authorization: Bearer <redacted — swarm test session> # --- server replied --- 200 OK · content-type: application/json { "learnerId": 48211, "name": "Test Learner B", # not account 48210 "grade": "4th", "mastery": 0.82, "attendance": "...", "reportCard": [ ... ] } # PROVED: account A read account B's record. Stopped here — no writes, no real users.
Both accounts are swarm-owned test learners seeded for this assessment. One read-only request, no data modified, no production user involved. Fully replayable.
⌁The one fix that shuts it down
async function getReport(req, res) { const { learnerId } = req.params; - const report = await db.reportFor(learnerId); + // only the learner themself (or their guardian/teacher) may read it + if (!canAccessLearner(req.user, learnerId)) + return res.status(403).json({ error: "forbidden" }); + const report = await db.reportFor(learnerId); return res.json(report); }