How a Read-Only Employee Account Exposed an Entire Workforce
Target class: SaaS workforce / scheduling platform. Access level: standard employee (lowest privilege). Severity: High. Company name and hostnames withheld; details generalized to protect the client.
This is one of the most common high-severity patterns we find, and one of the least likely to be caught by a scanner: broken function-level authorization on read paths. The application carefully checked who was allowed to change data — and never checked who was allowed to see it.
The setup
The platform lets organizations manage staff, schedules, and pay. Every user has a role — administrator, manager, or employee — and the UI is scrupulous about it: an employee only ever sees their own profile and their own schedule. The screens are locked down exactly as you'd expect.
We authenticated as a standard employee — the account type with the fewest rights — and put the mobile app behind a proxy to watch the API it actually spoke to. As always, the app was just a client; the real surface was the API.
The finding
The employee's own profile screen loaded from an endpoint of the shape:
GET /api/employees/{id}
That returned the logged-in user's record. Predictable. But the app also called a collection endpoint to populate internal lookups:
GET /api/employees
As a standard employee, that request returned the full organization directory — every colleague's complete record. Not names and desk numbers. Full records: home addresses, personal phone numbers, dates of birth, and hourly pay rates. Iterating /api/employees/{id} across sequential IDs returned the same, one record at a time, for anyone in the company.
The write side was airtight — attempting to modify another employee returned 401 and required a manager role. But the read side had no such check. Authorization had been implemented as "can this user edit this object?" and silently assumed that anyone who could reach the read endpoint was allowed to see everything it returned.
Why scanners miss it
There's no injection, no error, no anomalous response to flag. The endpoint returns 200 OK with well-formed JSON — exactly as designed. The only thing wrong is who received it, and a scanner has no concept of the intended authorization model. You find this by knowing what the role should be able to see, then checking what the API actually returns.
Impact
Any employee — including a brand-new, minimally-trusted account — could exfiltrate the personal and financial data of the entire workforce. For an organization of any size, that's a mass PII and compensation-data disclosure from the lowest rung of access. In regulated sectors it's also a direct compliance failure.
The fix
Authorization must be enforced on every data path, reads included, and scoped to what the requester's role is entitled to see:
- The collection endpoint must filter to records the caller is authorized for (an employee sees only themselves; a manager sees their team).
- The object endpoint must verify the caller may view that specific record, not merely that they're authenticated.
- Treat "can read" and "can write" as separate authorization decisions — a system that only guards writes is unguarded.
The lesson
When you test authorization, test it on reads with the same rigor as writes, and always with the least-privileged account you can get. The bugs that pay aren't in the admin panel — they're in the ordinary employee's app quietly returning everyone else's data.