| CVE | Vulnerability name | Date | Responsible Security Disclosure by | Vulnerabilities |
|---|---|---|---|---|
|
-
|
RandomBleed |
2026-07-17 |
GitHub CodeQL (code scanning alert #422) and xet7 (fix)
![]() Automated code scanning flagged the biased randomness. |
|
WeKan's startup schema upgrade (server/lib/schemaUpgradeSteps.js) creates new
documents — default swimlanes, rescued lists, extracted checklist items — for data migrated
from old WeKan versions, and gives each a Meteor-style 17-character random id so upgraded
documents look exactly like app-created ones. The id generator drew cryptographically secure
random bytes and mapped each byte to a character with modulo:
// vulnerable — modulo over a non-power-of-two alphabet is biased
const ID_CHARS = '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz'; // 55 chars
function randomId(len = 17) {
const bytes = crypto.randomBytes(len);
let out = '';
for (let i = 0; i < len; i++) out += ID_CHARS[bytes[i] % ID_CHARS.length];
return out;
}
A byte is a uniform value in 0..255, i.e. 256 possibilities. The alphabet has 55
characters, and 256 is not a multiple of 55: 256 = 4 × 55 + 36. So the
values 0..219 map four-to-one onto the 55 characters, but the leftover values
220..255 map an extra time onto the first 36 characters. Those first 36
characters therefore appear with probability 5/256 while the remaining 19 appear
with probability 4/256 — the first 36 are ~25% more likely per draw (~1.4% of the
total distribution each vs ~1.1%). The ids are still hard to guess, but the reduced entropy is
exactly what CodeQL's js/biased-cryptographic-random rule flags: a cryptographically
secure source is being used in a way that throws away part of its uniformity.
Fix: rejection sampling. Only accept bytes below the largest multiple of the alphabet
size (256 − (256 mod 55) = 220); discard and redraw anything in 220..255,
so every character is exactly equally likely. The ids stay Meteor-style 17 characters (and
exact-length for any requested length):
// fixed — rejection sampling removes the modulo bias
function randomId(len = 17) {
const limit = 256 - (256 % ID_CHARS.length); // 220
let out = '';
while (out.length < len) {
const bytes = crypto.randomBytes(len - out.length);
for (let i = 0; i < bytes.length && out.length < len; i++) {
if (bytes[i] < limit) out += ID_CHARS[bytes[i] % ID_CHARS.length];
}
}
return out;
}
A negative regression test pins that out-of-range bytes are resampled rather than wrapped, so
the biased modulo cannot silently return. CodeQL rule
js/biased-cryptographic-random was resolved by this change, and the whole codebase
was swept for other randomBytes modulo uses — this was the only one.