Hall of Fame image from https://openclipart.org/detail/120343/trophy
Back to Hall of Fame Contents Back to Wekan Website

Contents / HashBleed

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.
  • HashBleed — GET /api/users/{userId} and PUT /api/users/{userId} serialised the whole Meteor user document, so every answer carried services.password.bcrypt and the hash of every live session token (CWE-200 Exposure of Sensitive Information)
  • The authorisation was never the problem: both endpoints are admin-only and stayed that way. The PAYLOAD was. Walking the ids that GET /api/users returns harvested the offline crackable password hash and the session-token hashes of every account on the instance
  • A password hash is an offline attack that does not touch the server, and it survives long after the export. The session material is services.resume.loginTokens[], one entry per live session with the time it began
  • The two sibling endpoints in the same file show what was intended: GET /api/users projects down to _id and username, and GET /api/user, the self view, runs delete data.services before answering. Nothing in the code, the CHANGELOG or the documentation ever said the subtree was meant to be exposed
  • One helper strips services and sessionData now, and both endpoints answer through it — the PUT as well, which returned the same unprojected document after every action branch
  • Fixed at upcoming WeKan release


Details

HashBleed — an admin API that answered with credential material (CWE-200)

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.



Back to Hall of Fame Contents Back to Wekan Website