Sports PreGame: public defaults, auth oracles, and soft social takeovers
A second pass over public reads and auth — defaults that weren't access control, error messages that leaked too much, and social login that trusted the wrong signal.
Pass 1 was the loud stuff — SSRF-shaped fetches, mass assignment, upload hygiene. Pass 2 was quieter: a second pass over public reads and auth paths after the first fixes landed.
Nothing exotic. Mostly places where a default felt safe enough until you looked again.
Defaults are not access control
We had a public list endpoint that filtered by status. The default was the moderated, public-safe value. Fine for the happy path.
The problem was the filter still came from the client. Flip the query and you could ask for content that had never cleared review. Detail-by-id had a similar gap — it loaded the record without checking whether it was supposed to be public yet.
// before — client chooses the visibility filter
const status = req.query.status ?? 'approved';
const items = await Model.find({ status });
Fix: on public routes, hardcode the safe filter in the server. Ignore the client param. Pending or rejected IDs should look like missing resources, not “you found the queue.”
Admin tools keep their own authenticated surfaces. Public discover stays public discover.
Error messages can confirm a password
We had an account-reactivation path that re-checked credentials. Wrong password said one thing. Correct password on an account that wasn’t eligible for reactivation said another.
That difference is an oracle. Combined with lockout that only lived on the main login route, the reactivation path became a quieter place to test credentials.
Fix: same generic failure message on every failed check. Same attempt counter and lockout window as login. Successful reactivation resets the counter. If you burn attempts on a “valid password, wrong account state” case, that’s a deliberate tradeoff — better than teaching an attacker which guesses were right.
Don’t ship emails on public payloads
One public populate returned contributor profile fields that included email. Another nearby endpoint already returned name only. Scrapers don’t care that you meant well.
Fix: public responses get the minimum. Name if you need attribution. Email stays off the wire.
Social login still needs a verified email
If an email/password account already existed, a social provider could link and issue a session without a strong enough check that the provider had actually verified that email.
Auto-linking on email match alone is a soft account-takeover path. Low drama when the provider is strict — still wrong when the flag is missing or false.
Fix: refuse the link unless the provider says the email is verified. Ask the user to sign in with password instead. Normalize boolean vs string flags from different providers so you don’t silently skip the check.
Takeaways
- A default query value is not authorization. Pin public filters in code.
- Auth paths that check a password need the same failure shape and the same lockout.
- Public JSON should not include fields you’d hate to see in a dump.
- Social link ≠ “same email string.” Require verified.
More PreGame notes as we keep hardening. The private write-ups stay in the project notes — this log is the lessons, not the inventory.
Related
Author

Fawad Naeem
Full-stack developer in Lahore building React Native apps, Astro sites, and AWS-backed products. This log is where the shipping notes live.
View portfolio ↗Hire
Working on something?
If you want help shipping a mobile or web product, email me — or see past work on the portfolio.
Newsletter
Occasional shipping notes
No spam — just new build logs when they ship. Form is ready; provider wiring comes later.