Unauthenticated API Endpoint Exposing Password Hashes
API endpoint enumeration with a common wordlist surfaces paths that the application
doesn't link to from its UI but serves without authentication. This engagement found
/api/users/datatable — a backend data-table endpoint that returns the full
user list in a JSON envelope. The response included password hashes, password salts,
the hashing algorithm identifier, and the role description for every user in the
system. No authentication header was required.
The endpoint
An API enumeration pass against the API root with a common wordlist returned a 200 for
/api/users/datatable. The response was not paginated and returned all users.
The admin account record included the full credential material:
# api enumeration hit
[api-fuzzer] -u https://app.acme-lab.test/api/FUZZ -w api-common.txt
/api/users/datatable [200] [Content-Type: application/json]
# response (admin user record — lab values)
GET /api/users/datatable HTTP/1.1
Host: app.acme-lab.test
HTTP/1.1 200 OK
{
"users": [
{
"id": 1,
"username": "admin",
"email": "admin@acme-lab.test",
"passwordHash": "<lab-hash>",
"passwordSalt": "lab_salt_example",
"hashAlgorithm": "SHA-256",
"roleId": 1,
"roleDescription": "Full system access"
}
]
}
# SHA-256 with a static salt — crackable offline with rockyou.txt
# no authentication required to retrieve this response
SHA-256 is a fast general-purpose hash function. With GPU acceleration, billions of SHA-256 operations per second are achievable on commodity hardware. A password matching rockyou.txt at SHA-256 speed cracks in seconds. The hash here corresponds to a common default — "admin" — and the static salt does not prevent dictionary attacks, it only requires that the salt be included in the input, which is known from the response.
The RBAC bypass companion
A second finding from the same engagement: an RBAC bypass on the administrative panel where role assignment checks were enforced in the UI but not the API, allowing any authenticated user to escalate to administrator role by calling the user-update endpoint directly.
The test timeline
ffuf pass against the API root. Common API wordlist. /api/users/datatable returns 200 with no authentication header present.
Admin password hash, salt, and algorithm returned unauthenticated. SHA-256 with static salt. roleDescription: "Full system access" for user ID 1.
Role escalation via direct API call. UI enforces role check; API endpoint does not. Any authenticated user can assign themselves administrator role via the user-update API.
All user hashes assessed. Full user list returned — all password hashes exposed. Admin hash corresponds to a dictionary word. Offline crack confirmed in lab environment.
Both findings documented. Offline crack path captured with evidence. SHA-256 upgrade path recommended alongside access control fix.
Closing the exposure
- Require server-side authentication on every API endpoint, including internal and admin-facing paths. The presence of a path in a wordlist does not mean it must remain accessible. Add authentication middleware to the datatable route. Verify every API endpoint has an authentication check — not just the ones linked from the UI.
- Separate credential storage from user-listing responses. The user datatable endpoint should return identifiers, usernames, and roles — not hashes, salts, or algorithm identifiers. Credential fields must never appear in any response payload regardless of the requester's authentication state.
- Migrate password storage to Argon2id or bcrypt. SHA-256 is a fast hash designed for throughput, not password storage. Argon2id (the recommended choice from OWASP) and bcrypt are designed to be computationally expensive, making GPU-accelerated offline cracking orders of magnitude slower. Migrate on next login or in a scheduled background job.
- Run an API enumeration pass before every production deploy. Tools like ffuf with a common API wordlist take minutes to run and surface exactly this class of finding. Adding it to the deployment pipeline or pre-release checklist would have caught this endpoint before it reached production.
The takeaway
The endpoint was not hidden — it was in a common wordlist, returning 200, with no authentication. Admin credentials in the response body, no auth required, fast hash. Discovery: one ffuf pass. Crack: seconds.