| CVE | Vulnerability name | Date | Responsible Security Disclosure by | Vulnerabilities |
|---|---|---|---|---|
|
GHSA-6qpx-x7vr-p9w6
|
HashBleed |
2026-08-15 |
ybsun0215 and xet7
![]() Coordinated disclosure via GitHub Security Advisory GHSA-6qpx-x7vr-p9w6. |
|
WeKan exposes two admin-only endpoints for a single user:
GET /api/users/{userId}
PUT /api/users/{userId}
Both fetched the user with no field projection and serialised the result:
// server/models/users.js — before
await Authentication.checkUserId(req.userId); // admin-only: correct
let user = await ReactiveCache.getUser({ _id: id }); // no projection
...
sendJsonResult(res, { code: 200, data: user }); // the whole document
Which means the answer looked like this:
{
"_id": "tFTzffMDuWKh73czQ",
"username": "victim",
"services": {
"password": {"bcrypt": "$2b$10$pJANilpe8W2rLOjNt1g4w..."},
"resume": {"loginTokens": [
{"when": "...", "hashedToken": "gP65OoMK+OZ3GuIE/uri6/59OZy0/q4qk+CaGkQm8Xo="}
]}
}
}
An admin is trusted with a lot, but not with every user's password hash: the point of hashing is that nobody holds the material, including the operator. And an admin token that leaks becomes the credentials of the whole instance rather than one account.
The fix is the projection the sibling endpoints already had, in one place both use:
// server/models/users.js — after
function withoutSecrets(user) {
if (!user || typeof user !== 'object') return user;
delete user.services;
delete user.sessionData;
return user;
}
tests/restApiIdorBatch.test.cjs pins that both endpoints answer through it, and that the self view and the list endpoint keep the projections they always had.