critical Threat analysis

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.

#drupal#cisa-kev#zero-day#vulnerability-response#sql-injection
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:N for 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

    ClassificationCriteriaRequired evidenceRemediation triggerClosure condition
    Confirmed compromiseDrupal 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 exposedDrupal 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 exposedDrupal 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 exposedNo 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.
    UnknownVersion, 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))

    Sources

    1. CISA Known Exploited Vulnerabilities catalog JSON
    2. Drupal SA-CORE-2026-004
    3. NVD CVE-2026-9082