
Username Enumeration via Authentication Timing Side-Channel in PaperCut NG
| Field | Details |
|---|---|
| CVE ID | CVE-2026-8794 |
| Product | PaperCut NG |
| Affected Version | 25.0.11 (Build 75758) and earlier |
| CWE | CWE-208 — Observable Timing Discrepancy |
| CVSS Score | 3.7 (Low) — standalone; escalates when chained with CVE-2026-8793 |
| Researcher | Vivien LEBAS (@Hazaz) |
| Reported | May 2026 |
| Status | Patched — PaperCut NG 26.0.3 |
PaperCut NG's authentication endpoint exhibits a measurable timing discrepancy between login attempts against existing and non-existent accounts. This discrepancy — caused by bcrypt password hashing only being performed when a matching account is found — allows an unauthenticated attacker to reliably determine whether a given username exists in the system by measuring HTTP response times.
When combined with CVE-2026-8793 (absence of brute-force protection), this vulnerability enables a complete credential-compromise attack chain: enumerate valid usernames first, then perform unlimited targeted password guessing with no risk of lockout.
The timing discrepancy stems from a classic implementation pattern: the application only invokes bcrypt comparison when an account matching the submitted username is found in the database. For non-existent accounts, the authentication routine exits early — before bcrypt is called — resulting in a measurably shorter response time.
Valid username: DB lookup (found) → bcrypt comparison → failure response ~2.1s avg
Invalid username: DB lookup (not found) → immediate failure response ~0.2s avg
This pattern is well-documented (see CWE-208). The standard remediation is to perform a dummy bcrypt comparison even when the account does not exist, equalising response times across both code paths.
Authentication attempts were made against the PaperCut NG login endpoint under controlled conditions:
curl with --write-out "%{time_total}", intentionally wrong password in all casesThe two distributions show zero overlap, making enumeration reliable even over moderate network jitter.
# Measure response time for an existing account (wrong password)
curl -s -o /dev/null -w "%{time_total}\n" \
-X POST "http://<target>:9191/app" \
-H "Origin: http://<target>:9191" \
--data "service=direct/1/Home/%24Form&inputUsername=admin&inputPassword=wrongpassword"
# Output: ~2.1s
# Measure response time for a non-existent account
curl -s -o /dev/null -w "%{time_total}\n" \
-X POST "http://<target>:9191/app" \
-H "Origin: http://<target>:9191" \
--data "service=direct/1/Home/%24Form&inputUsername=zz_doesnotexist_zz&inputPassword=wrongpassword"
# Output: ~0.2s
A response time significantly above ~0.5s reliably indicates a valid username. A simple script can automate enumeration against a wordlist of common corporate naming patterns (firstname.lastname, flastname, etc.).
This finding is most impactful when used as the first step in a two-stage attack:
Step 1 (CVE-2026-8794, this finding) — Build a list of valid PaperCut NG account names by timing authentication responses. In corporate environments, a short wordlist of firstname.lastname combinations derived from public sources (LinkedIn, company website, email footers) is often sufficient.
Step 2 (CVE-2026-8793) — Submit unlimited password attempts against confirmed valid accounts. The absence of rate-limiting or lockout means any credential-stuffing or password-spraying attack can be conducted entirely online at full speed.
An unauthenticated attacker with network access to TCP/9191 can:
In typical corporate deployments, PaperCut usernames mirror Active Directory accounts. A confirmed valid PaperCut username is therefore also a confirmed valid domain account name, amplifying the reconnaissance value beyond PaperCut itself.
For administrators (interim mitigations):
For the vendor:
Perform a constant-time dummy bcrypt comparison when the submitted username does not match any account, ensuring response times are equalised regardless of whether the account exists:
# Pseudocode — constant-time authentication pattern
user = db.find_user(username)
if user:
valid = bcrypt.verify(password, user.password_hash)
else:
bcrypt.verify(password, DUMMY_HASH) # always run, result discarded
valid = False
This is a well-established pattern for mitigating CWE-208 in authentication flows.
| Date | Event |
|---|---|
| May 9, 2026 | Initial report submitted to PaperCut Security Team |
| May 10, 2026 |
Disclosed in accordance with responsible disclosure principles. Full technical details were shared with the PaperCut Security Team prior to public release.
Researcher: Vivien LEBAS — @Hazaz
| Condition | Min (s) | Max (s) | Mean (s) | Std Dev |
|---|
Existing account (admin) | 2.08 | 2.19 | 2.13 | 0.031 |
Non-existent account (zz_doesnotexist_zz) | 0.18 | 0.24 | 0.21 | 0.018 |
| Difference | ~1.92s |
| Acknowledgment received |
| May 2026 | CVE-2026-8794 assigned by PaperCut (CNA) |
| August 2026 | Patch released — PaperCut NG 26.0.3 |
| August 2026 | Public disclosure coordinated with vendor |