| Título | dgtlmoon changedetection.io 0.55.8 Race Condition |
|---|
| Descrição | # Timing Attack Vulnerability in Password Verification (CWE-208)
**BUG_Author:** herantong
**Affected Version:** changedetection.io ≤ 0.55.8
**Vendor:** [changedetection.io GitHub Repository](https://github.com/dgtlmoon/changedetection.io)
**Software:** [changedetection.io](https://github.com/dgtlmoon/changedetection.io)
**Vulnerability Files:**
- `changedetectionio/flask_app.py`
---
## Description
### 1. Timing Side-Channel in Password Hash Comparison
In `flask_app.py`, the `check_password()` method compares a PBKDF2-derived key against a stored salted hash using Python's standard `==` operator. This operator performs a lexicographic, short-circuit byte comparison that returns `False` as soon as the first mismatched byte is encountered. Consequently, the response time of a failed login attempt is proportional to the number of leading bytes that match the correct hash, enabling a timing side-channel attack (CWE-208).
### 2. Exploiting the Timing Side-Channel
An attacker with network access to the application can repeatedly send crafted login requests while measuring the response latency of each attempt. By brute-forcing one byte position at a time and observing which candidate produces the longest response time, the attacker can iteratively recover the full password hash. Once the hash is known, it can be used to construct valid authentication material.
### 3. Vulnerable Code Location
The vulnerability resides in the `check_password` method of the `User` class:
```python
# changedetectionio/flask_app.py:482-504
def check_password(self, password):
import base64
import hashlib
raw_salt_pass = os.getenv("SALTED_PASS", False)
if not raw_salt_pass:
raw_salt_pass = datastore.data['settings']['application'].get('password')
raw_salt_pass = base64.b64decode(raw_salt_pass)
salt_from_storage = raw_salt_pass[:32]
new_key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt_from_storage,
100000
)
new_key = salt_from_storage + new_key
return new_key == raw_salt_pass # <-- Vulnerable comparison on line 504
```
### 4. Reachability via HTTP Endpoint
The vulnerable method is directly invoked by the `/login` POST handler, which accepts user-controlled input:
```python
# changedetectionio/flask_app.py:742-775
@app.route('/login', methods=['GET', 'POST'])
def login():
# ...
password = request.form.get('password')
if (user.check_password(password)): # <-- User input reaches vulnerable comparison
flask_login.login_user(user, remember=True)
return redirect(validated_redirect)
else:
flash(gettext('Incorrect password'), 'error')
return redirect(url_for('login', redirect=redirect_url if redirect_url else None))
```
### 5. Absence of Mitigations
- No usage of `hmac.compare_digest` or `secrets.compare_digest` was found anywhere in the codebase.
- No rate-limiting, account lockout, or network-level defenses are in place for the `/login` endpoint.
- The project requires Python ≥ 3.10, making both `hmac.compare_digest` (since Python 3.3) and `secrets.compare_digest` (since Python 3.6) readily available.
---
## Proof of Concept
1. Identify the login endpoint of the target changedetection.io instance:
```
POST http://<target-host>/login
```
2. Send repeated login requests with a fixed candidate password while measuring the response time of each attempt. A higher response time for a particular byte value indicates a correct prefix match against the stored hash.
3. Example timing measurement approach (pseudocode):
```python
import time
import requests
target = "http://<target-host>/login"
recovered_prefix = b""
hash_length = 64 # PBKDF2-HMAC-SHA256 output + 32-byte salt
for position in range(hash_length):
best_byte = None
best_time = 0
for candidate in range(256):
password = b"placeholder"
start = time.perf_counter()
requests.post(target, data={"password": password})
elapsed = time.perf_counter() - start
if elapsed > best_time:
best_time = elapsed
best_byte = candidate
recovered_prefix += bytes([best_byte])
```
4. After iterating through all byte positions, the attacker fully recovers the stored salted hash, allowing authentication bypass.
---
## Root Cause Analysis
| Question | Answer |
|---|---|
| Does the code perform a cryptographic comparison on a security-sensitive value? | Yes — `new_key == raw_salt_pass` on line 504 compares a PBKDF2-derived password hash. |
| Is the comparison performed using a non-constant-time operator? | Yes — Python's `==` short-circuits on the first mismatched byte. |
| Does the compared data originate from user input? | Yes — the `password` parameter comes from `request.form.get('password')` on line 765. |
| Is the vulnerable code reachable from an external HTTP endpoint? | Yes — it is called directly by the `/login` POST handler on line 767. |
| Are any mitigations already in place? | No — no `compare_digest`, rate limiting, or account lockout exists. |
| Is the code in a test, demo, or dead-code context? | No — it is the live production authentication path. |
**Verdict: Confirmed vulnerability (CWE-208 — Observable Timing Discrepancy).**
---
## Fix Recommendations
1. **Primary Fix**: Replace the `==` comparison on line 504 with a constant-time comparison function:
```python
import hmac
# ...
return hmac.compare_digest(new_key, raw_salt_pass)
```
Alternatively, `secrets.compare_digest(new_key, raw_salt_pass)` (available since Python 3.6).
2. **Verification**: Run the existing test suite (e.g., `test_security.py`) to confirm that the login flow still passes. Add a unit test that asserts `check_password` returns `False` for an incorrect password without raising exceptions.
3. **Defense in Depth** (optional but recommended): Add rate limiting or account lockout on the `/login` endpoint to further reduce the feasibility of network-based timing attacks.
---
## References
- [CWE-208: Observable Timing Discrepancy](https://cwe.mitre.org/data/definitions/208.html)
- [Python `hmac.compare_digest` documentation](https://docs.python.org/3/library/hmac.html#hmac.compare_digest)
- [Python `secrets.compare_digest` documentation](https://docs.python.org/3/library/secrets.html#secrets.compare_digest)
|
|---|
| Fonte | ⚠️ https://github.com/herantong/cve/blob/main/Timing%20Attack%20Vulnerability%20in%20Password%20Verification%20(CWE-208) |
|---|
| Utilizador | herantong (UID 97028) |
|---|
| Submissão | 20/07/2026 04h01 (há 2 meses) |
|---|
| Moderação | 22/09/2026 07h03 (2 months later) |
|---|
| Estado | Aceite |
|---|
| Entrada VulDB | 408338 [dgtlmoon changedetection.io até 0.60.7 Hash Comparison flask_app.py check_password Senha Divulgação de Informação] |
|---|
| Pontos | 20 |
|---|