Weak PRNG for Security Tokens
Summary
BDServer uses Python's random module to generate session tokens, email verification codes, and other security-sensitive values. The random module uses the Mersenne Twister (MT19937) algorithm, which is not cryptographically secure. In theory, an attacker who observes enough consecutive PRNG outputs can recover the internal state and predict future tokens. In practice, several factors make this attack difficult to execute.
Vulnerability Details
| Property | Value |
|---|---|
| Type | Use of Cryptographically Weak PRNG |
| CVSS | 7.4 (High) |
| CWE | CWE-338 (Use of Cryptographically Weak PRNG) |
Root Cause
The RandString() function generates random strings using random.choice():
import random
def RandString(n=50):
# this will make a random string
s = ""
good = "QWERTYUIOPASDFGHJKLZXCVBNM1234567890"
for i in range(n):
s = s + random.choice(good)
return s
This function is used for security-sensitive purposes throughout the codebase:
| Usage | Location | Length |
|---|---|---|
| Session tokens | Render.py:67 |
200 chars |
| Email verification | Email.py:102, Email.py:204 |
10 chars |
| Invite codes | Render.py:5179 |
50 chars |
| Private key password | ActivityPub.py:124 |
200 chars |
The Theoretical Attack
MT19937 maintains 624 32-bit integers as its internal state. Each time the PRNG generates a number, it transforms one of these integers through a series of bitwise operations before outputting it. These operations are reversible: given an output, you can compute the original state integer.
If an attacker collects 624 consecutive outputs, they can reverse each one to recover the full internal state. With the state recovered, they can clone the PRNG and predict all future outputs.
Each call to random.choice() consumes PRNG state, so a 200-character token requires 200 calls. The exact number of underlying MT19937 outputs consumed depends on Python's implementation details and rejection sampling, but collecting several consecutive tokens would provide enough data for state recovery.
Practical Difficulties
Several factors make this attack harder to execute in the wild:
Other PRNG consumers: The codebase has multiple other calls to random that consume PRNG state:
| Location | Function | When Called |
|---|---|---|
Common.py:311-313 |
IDcolor() |
Every new session (3 calls) |
Render.py:362 |
random.choice() |
Article selection |
Render.py:383, Render.py:458 |
random.uniform() |
Content scoring (L383 uses alsorandom, an alias for random) |
ActivityPub.py:185 |
random.uniform() |
Federation timing |
Analyse.py:554 |
random.random() |
User sorting |
Any request that triggers these functions will consume PRNG outputs, breaking the "consecutive outputs" requirement.
Rejection sampling uncertainty: When random.choice() selects from 36 characters, it generates random numbers and discards any that fall outside the valid range. The attacker observes only the characters that were selected, not the discarded values. This creates uncertainty when trying to map observed tokens back to the underlying PRNG outputs, requiring specialized constraint-solving software to work through the possibilities.
Server restarts: Any server restart reseeds the PRNG, invalidating collected data.
Timing requirements: The attacker must collect tokens, perform state recovery, and predict the next token before another user or background process consumes PRNG state.
Realistic Assessment
On a low-traffic BDServer instance with no other activity, this attack is feasible. On an active server with multiple users and background processes, unpredictable PRNG state consumption makes the attack significantly harder.
The vulnerability represents a real cryptographic weakness, but practical exploitation requires favorable conditions.
Reproducing the Vulnerability
In a controlled environment with direct access to the PRNG, state recovery works reliably: collect 624 outputs, reverse the bitwise transformations on each, and use the recovered state to predict future values.
Against a live BDServer instance, the practical difficulties described above (other PRNG consumers, rejection sampling, timing) make successful exploitation unlikely without custom tooling and favorable conditions.
Remediation
Replace random with secrets in Common.py:
import secrets
import string
def RandString(n=50):
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for _ in range(n))
The secrets module uses os.urandom(), a cryptographically secure random source with no recoverable state.
Disclaimer
This assessment was performed on a best-effort basis against BDServer commit dc86854 and reflects the state of the software at the time of testing. The findings and remediations are provided for informational purposes and should be independently validated before implementation. This report does not guarantee all vulnerabilities have been identified, nor does it guarantee the suggested fixes will be effective in all environments.