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

Contents / JamBleed

CVE Vulnerability name Date Responsible Security Disclosure by Vulnerabilities
GHSA-rf3w-rj48-jxcc

JamBleed

2026-08-16 daniais and xet7

Coordinated disclosure through a GitHub security advisory.
  • JamBleed — the brute-force lockout counted an attacker's failed logins against the VICTIM'S ACCOUNT rather than against the address they came from, so anyone who knew a username could lock its owner out of login from every address, repeatably (CWE-307)
  • A correct password was refused while the lock held, and counted as a further failure on the way
  • Affected v10.59 and newer. No confidentiality or integrity impact; the loss is the availability of authentication for a chosen user
  • Fixed at upcoming WeKan release


Details

JamBleed — the lock that the attacker closes and the owner cannot open (CWE-307)

A brute-force lockout exists to stop somebody guessing a password. WeKan's counted the guesses against the wrong party. wekan-accounts-lockout kept ONE counter per user:

services.accounts-lockout.failedAttempts
    

with no notion of where the attempts came from. Three wrong passwords — the default failuresBeforeLockout — and the account was locked from every address, for the lockout period, repeatably. An attacker who knew a username could hold a target out of their own account for as long as they cared to keep going.

Usernames are not a secret in WeKan: board and card members are listed to everyone who can see the board. Choosing a target was trivial, an administrator was as easy to lock out as anyone else, and walking the member list locks out an entire instance. The reporter demonstrated it on a clean v10.91 lab instance:

attacker IP A : Incorrect / Incorrect / Too many attempts
victim  IP B  : (correct password) Too many attempts   <- locked from every IP
    

The second fault, which is the one that hurts

That last line is not only the global counter. The hook allowed a login attempt through when there was no error and no lock:

// packages/wekan-accounts-lockout/src/knownUser.js — before
const unlockTime = KnownUser.unlockTime(loginInfo.user);
if (loginInfo.error === undefined && unlockTime === 0) {
  return loginInfo.allowed;
}
    

So during a lock the real owner, typing the right password, fell through to the same Too many attempts refusal as the attacker — and incremented the counter that was locking them out on the way past. A lock is meant to stop guessing. Somebody who did not have to guess has proved they are not the attacker.

Why v10.59 and not earlier

The flat counter has existed since 2022, but until v10.59 the lockout was inert: its hooks gated on the English strings Incorrect password and User not found, which Meteor's ambiguousErrorMessages rewrites before the hooks run. The counter never moved and no account ever locked. The LockoutBleed fix in v10.59 made the lockout count real password failures again — which is what made this reachable.

That is worth stating plainly rather than burying: a fix that turns on a mechanism inherits whatever that mechanism gets wrong. Repairing the brute-force protection was correct, and it exposed a design fault that had been sitting inert underneath it for three years.

The fix

The counter is per (user, source address). An attacker locks out the address they are attacking from, which is the address that should be locked, and the person whose account it is keeps logging in from theirs.

services.accounts-lockout.byAddress.<sha256-prefix>.{failedAttempts, unlockTime, ...}
    

The address comes from X-Forwarded-For under HTTP_FORWARDED_COUNT — the same rule the REST login throttle uses, so a lockout and a throttle cannot disagree about who somebody is — and only the position hops from the right is read, so a forged header cannot pick its own bucket and hand an attacker a fresh counter for every attempt. The key is hashed for two reasons: an IPv4 address is all dots and cannot be a Mongo field name at all, and a locked account should not accumulate a list of the addresses that attacked it, which is a record of who was where that WeKan has no reason to keep.

And a correct password is allowed first, before anything reads the lock, clearing the state behind it:

// after
if (!hadError) {
  return { action: 'allow', clearScope: true };
}
    

It clears the pre-fix flat fields too, so an account left locked by the old global counter is freed by its owner's next correct login rather than by a wait with no visible end. Hammering during a lock no longer extends it either — otherwise the denial of service returns inside the mechanism meant to stop it.

A lockout firing is recorded and shows in Admin Panel → Problems, so an administrator can see that somebody tried. On the lock only, not on every refused attempt during one: an attacker could otherwise fill that page by holding down a key.

The decision now lives in a pure module, so tests/lockoutPerSourceAddress.test.cjs reproduces the reported attack as arithmetic rather than through a running server — nineteen tests covering the attack itself, the correct password during a lock, that the lock still fires and still expires, the forwarded-header rules, that malformed state reads as nothing yet rather than throwing (a lockout that threw on an unexpected document would lock everybody out of a database that had one), and that every construction of the lockout passes the reporter, since there are two and a settings reload that dropped it would stop recording attempts while the guard kept working.

How it was reported

daniais reported it through a GitHub security advisory with a lab reproduction on v10.91, an A/B harness comparing the original against a patched build, a suggested fix direction, and a CVSS assessment they marked as preliminary and offered for independent review. They offered a 90-day disclosure window rather than assuming one. Nothing was published before the fix.



Back to Hall of Fame Contents Back to Wekan Website