Drupal Core CVE-2026-9082: KEV SQL Injection Exposure
CISA added Drupal Core CVE-2026-9082 to KEV on 2026-05-22. The exploitable surface is PostgreSQL-backed Drupal Core in affected 8.9.x, 10.x, and 11.x ranges; this article provides composer, settings, and telemetry scripts for exposure and closure.
On this page 0% read
Executive Summary
CISA added CVE-2026-9082 to KEV on 2026-05-22 with a due date of 2026-05-27 CISA KEV. Drupal’s advisory maps the vulnerable surface to Drupal Core’s database abstraction API when object input reaches multi-value IN conditions, with PostgreSQL-backed deployments the relevant database scope Drupal.
Public primary sources prove KEV exploitation status and affected/fixed versions. They do not prove a public true-zero-day timeline, so the article treats zero_day_status as unproven from primary sources.
Key Facts
cve: "CVE-2026-9082"
vendor: "Drupal"
product: "Core"
kev_added: "2026-05-22"
kev_due: "2026-05-27"
kev_catalog_version: "2026.05.22"
vulnerability: "SQL injection in Drupal Core database abstraction API"
cwe: ["CWE-89"]
database_scope: "PostgreSQL-backed Drupal Core deployments"
affected_versions:
- "8.9.0 <= Drupal < 10.4.10"
- "10.5.0 <= Drupal < 10.5.10"
- "10.6.0 <= Drupal < 10.6.9"
- "11.1.0 <= Drupal < 11.1.10"
- "11.2.0 <= Drupal < 11.2.12"
- "11.3.0 <= Drupal < 11.3.10"
fixed_versions: ["10.4.10", "10.5.10", "10.6.9", "11.1.10", "11.2.12", "11.3.10"]
nvd_cvss_v31: "6.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N"
exploitation_status: "cisa_kev_exploited"
zero_day_status: "unproven_from_public_primary_sources"
Source Confidence & Evidence Mapping
- confirmed: CISA KEV lists CVE-2026-9082 as a known exploited vulnerability added on 2026-05-22 CISA KEV.
- confirmed: Drupal SA-CORE-2026-004 lists the affected Core version ranges and fixed releases Drupal.
- confirmed: NVD lists CWE-89 and CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:Nfor CVE-2026-9082 NVD. - unclear: Public primary sources do not provide a universal vulnerable route or a verified pre-patch exploitation start time.
Impact Determination
| Classification | Criteria | Required evidence | Remediation trigger | Closure condition |
|---|---|---|---|---|
| Confirmed compromise | Drupal telemetry, database telemetry, or host telemetry shows exploitation selectors tied to a vulnerable PostgreSQL-backed Drupal Core version. | Affected Drupal version, PostgreSQL driver evidence, timestamped telemetry line, and host/site identity. | Preserve Drupal files, database audit output, and web/proxy telemetry for the affected site. | Fixed Drupal Core version is installed and downstream audit script has no unexplained privileged account, file-write, or database activity. |
| Presumed exposed | Drupal Core is in an affected range and the site uses PostgreSQL. | composer.lock or composer show output plus settings evidence for pgsql or PostgreSQL. | Treat the site as exposed until fixed-version verification succeeds. | Script output shows a listed fixed release or an unaffected non-PostgreSQL deployment. |
| Potentially exposed | Drupal Core appears in source, lockfiles, CMDB, or scanner exports, but version or database driver is missing. | Repository, asset, scanner, or deployment evidence naming drupal/core or CVE-2026-9082. | Collect version and database driver evidence. | Asset resolves to confirmed compromise, presumed exposed, not exposed, or unknown. |
| Not exposed | No Drupal Core selector, affected version, PostgreSQL selector, or CVE-2026-9082 scanner row appears in complete asset exports. | Negative outputs from repository and scanner scripts. | None for this CVE. | Evidence bundle covers production, staging, CI images, and source lockfiles. |
| Unknown | Version, database driver, or telemetry exports are unavailable. | Gap statement naming the unavailable inventory or telemetry. | Keep internet-facing Drupal assets in scope until the missing evidence is recovered. | Evidence is recovered or the risk owner accepts the named gap. |
Timeline
- 2026-05-20: NVD publication timestamp for CVE-2026-9082 NVD.
- 2026-05-20: Drupal publishes SA-CORE-2026-004 with fixed Drupal Core releases Drupal.
- 2026-05-22: CISA adds CVE-2026-9082 to KEV with due date 2026-05-27 CISA KEV.
What Happened
The vulnerability is in Drupal Core’s database abstraction behavior, not in a single universal URL path. The useful scoping anchors are exact Core versions, PostgreSQL use, the SA-CORE-2026-004 advisory ID, and scanner rows for CVE-2026-9082.
Technical Analysis
The likely enterprise failure mode is an affected drupal/core package in a production or staging site backed by PostgreSQL. Drupal’s advisory also calls out contributed or custom code paths that pass untrusted object data into multi-value IN conditions. The scripts below do not attempt exploit reproduction; they classify inventory, database driver evidence, and post-exposure artifacts.
Affected Assets and Blast Radius
asset_selectors:
- "drupal/core"
- "Drupal Core"
- "SA-CORE-2026-004"
- "CVE-2026-9082"
- "pgsql"
- "PostgreSQL"
highest_value_assets:
- "internet-facing Drupal sites using PostgreSQL"
- "Drupal deployments with custom or contributed modules using database abstraction API IN conditions"
- "CI images and deployment artifacts containing affected drupal/core versions"
credentials_and_data_at_risk:
- "Drupal administrative sessions"
- "Drupal user records and roles"
- "PostgreSQL data reachable by the Drupal application account"
- "webroot files writable by the Drupal runtime user"
Indicators And Detection Selectors
cves: ["CVE-2026-9082"]
advisory_ids: ["SA-CORE-2026-004"]
packages: ["drupal/core"]
fixed_versions: ["10.4.10", "10.5.10", "10.6.9", "11.1.10", "11.2.12", "11.3.10"]
database_selectors: ["pgsql", "PostgreSQL"]
telemetry_selectors:
- "CVE-2026-9082"
- "SA-CORE-2026-004"
- "drupal/core"
- "user_role"
- "uid=1"
- "sites/default/files"
Detection and Hunting
#!/usr/bin/env python3
import json
import os
import re
import sys
from pathlib import Path
ROOT = Path(os.environ.get("ROOT", sys.argv[1] if len(sys.argv) > 1 else ".")).resolve()
TELEMETRY_DIR = Path(os.environ.get("TELEMETRY_DIR", "telemetry-export")).resolve()
OUT = Path(os.environ.get("OUT", "hp-drupal-cve-2026-9082-scope")).resolve()
CVE = "CVE-2026-9082"
ADVISORY = "SA-CORE-2026-004"
FIXED = ["10.4.10", "10.5.10", "10.6.9", "11.1.10", "11.2.12", "11.3.10"]
SELECTORS = [CVE, ADVISORY, "drupal/core", "Drupal Core", "pgsql", "PostgreSQL", "user_role", "uid=1", "sites/default/files"]
def vt(value):
return tuple(int(x) for x in re.findall(r"\d+", str(value))[:4])
def lt(left, right):
l, r = vt(left), vt(right)
width = max(len(l), len(r), 1)
return l + (0,) * (width - len(l)) < r + (0,) * (width - len(r))
def le(left, right):
return str(left) == str(right) or lt(left, right)
def vulnerable(version):
ranges = [("8.9.0", "10.4.10"), ("10.5.0", "10.5.10"), ("10.6.0", "10.6.9"), ("11.1.0", "11.1.10"), ("11.2.0", "11.2.12"), ("11.3.0", "11.3.10")]
return any(le(start, version) and lt(version, end) for start, end in ranges)
def read_text(path):
try:
return path.read_text(encoding="utf-8", errors="ignore")
except Exception:
return ""
OUT.mkdir(parents=True, exist_ok=True)
findings = {"composer_locks": [], "repository_matches": [], "telemetry_matches": []}
for lockfile in ROOT.rglob("composer.lock"):
data = json.loads(read_text(lockfile) or "{}")
packages = data.get("packages", []) + data.get("packages-dev", [])
for package in packages:
if package.get("name") == "drupal/core":
version = str(package.get("version", "")).lstrip("v")
settings_hits = []
for settings in lockfile.parent.rglob("settings.php"):
body = read_text(settings)
if "pgsql" in body or "PostgreSQL" in body:
settings_hits.append(str(settings))
findings["composer_locks"].append({
"file": str(lockfile),
"package": "drupal/core",
"version": version,
"cve": CVE,
"vulnerable_version": vulnerable(version),
"postgresql_settings_files": settings_hits,
"fixed_versions": FIXED,
})
for path in ROOT.rglob("*"):
if not path.is_file() or any(part in {".git", "node_modules", "vendor", "dist"} for part in path.parts):
continue
if path.suffix.lower() not in {".json", ".lock", ".php", ".yml", ".yaml", ".txt", ".md"}:
continue
body = read_text(path)
for selector in SELECTORS:
if selector in body:
findings["repository_matches"].append({"file": str(path), "selector": selector})
if TELEMETRY_DIR.exists():
for path in TELEMETRY_DIR.rglob("*"):
if path.is_file():
for line_no, line in enumerate(read_text(path).splitlines(), start=1):
for selector in SELECTORS:
if selector in line:
findings["telemetry_matches"].append({"file": str(path), "line": line_no, "selector": selector, "evidence": line[:1000]})
(OUT / "drupal-cve-2026-9082-scope.json").write_text(json.dumps(findings, indent=2, sort_keys=True), encoding="utf-8")
# Positive signal: drupal/core in an affected range plus pgsql/PostgreSQL evidence, or any scanner/telemetry row naming CVE-2026-9082.
print(json.dumps({"out": str(OUT), "composer_locks": len(findings["composer_locks"]), "telemetry_matches": len(findings["telemetry_matches"])}, indent=2))
Patch, Mitigation, and Verification
#!/usr/bin/env python3
import json
import os
import re
import sys
from pathlib import Path
ROOT = Path(os.environ.get("ROOT", sys.argv[1] if len(sys.argv) > 1 else ".")).resolve()
OUT = Path(os.environ.get("OUT", "hp-drupal-cve-2026-9082-closure")).resolve()
CVE = "CVE-2026-9082"
FIXED = ["10.4.10", "10.5.10", "10.6.9", "11.1.10", "11.2.12", "11.3.10"]
SOURCE = "https://www.drupal.org/sa-core-2026-004"
def vt(value):
return tuple(int(x) for x in re.findall(r"\d+", str(value))[:4])
def lt(left, right):
l, r = vt(left), vt(right)
width = max(len(l), len(r), 1)
return l + (0,) * (width - len(l)) < r + (0,) * (width - len(r))
def ge(left, right):
return not lt(left, right)
def fixed_or_unaffected(version):
if version.startswith("7."):
return True
fixed_ranges = [("10.4.10", "10.5.0"), ("10.5.10", "10.6.0"), ("10.6.9", "11.0.0"), ("11.1.10", "11.2.0"), ("11.2.12", "11.3.0"), ("11.3.10", "12.0.0")]
return any(ge(version, start) and lt(version, end) for start, end in fixed_ranges)
OUT.mkdir(parents=True, exist_ok=True)
results = []
for lockfile in ROOT.rglob("composer.lock"):
data = json.loads(lockfile.read_text(encoding="utf-8", errors="ignore"))
for package in data.get("packages", []) + data.get("packages-dev", []):
if package.get("name") == "drupal/core":
version = str(package.get("version", "")).lstrip("v")
results.append({
"cve": CVE,
"source": SOURCE,
"file": str(lockfile),
"installed_version": version,
"accepted_fixed_versions": FIXED,
"fixed_or_unaffected": fixed_or_unaffected(version),
})
(OUT / "drupal-cve-2026-9082-patch-verification.json").write_text(json.dumps(results, indent=2, sort_keys=True), encoding="utf-8")
# Remediation trigger: fixed_or_unaffected false for any PostgreSQL-backed Drupal Core deployment keeps the site open for CVE-2026-9082.
print(json.dumps({"out": str(OUT), "checked": len(results), "not_closed": [r for r in results if not r["fixed_or_unaffected"]]}, indent=2))
Downstream Abuse Audits
#!/usr/bin/env python3
import json
import os
import sys
from pathlib import Path
TELEMETRY_DIR = Path(os.environ.get("TELEMETRY_DIR", sys.argv[1] if len(sys.argv) > 1 else "telemetry-export")).resolve()
OUT = Path(os.environ.get("OUT", "hp-drupal-cve-2026-9082-downstream")).resolve()
CVE = "CVE-2026-9082"
SELECTORS = [
"SA-CORE-2026-004",
"CVE-2026-9082",
"user_role",
"users_field_data",
"uid=1",
"administer users",
"sites/default/files",
"settings.php",
"pgsql",
"PostgreSQL",
]
if not TELEMETRY_DIR.exists():
raise SystemExit(f"TELEMETRY_DIR not found: {TELEMETRY_DIR}")
OUT.mkdir(parents=True, exist_ok=True)
matches = []
for path in TELEMETRY_DIR.rglob("*"):
if not path.is_file():
continue
body = path.read_text(encoding="utf-8", errors="ignore")
for line_no, line in enumerate(body.splitlines(), start=1):
for selector in SELECTORS:
if selector in line:
matches.append({"cve": CVE, "file": str(path), "line": line_no, "selector": selector, "evidence": line[:1000]})
(OUT / "drupal-cve-2026-9082-downstream-selectors.json").write_text(json.dumps(matches, indent=2, sort_keys=True), encoding="utf-8")
# Positive signal: post-exposure Drupal role, user, file-write, settings.php, or PostgreSQL activity on an affected site.
# Remediation trigger: privileged account changes or unexplained webroot writes after first exposure keep Drupal sessions, database accounts, and deployment keys in scope.
print(json.dumps({"out": str(OUT), "matches": len(matches)}, indent=2))