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

    Script: local repository and exported telemetry scope

    #!/usr/bin/env python3
    import os
    import sys
    import json
    import subprocess
    from pathlib import Path
    
    ROOT = sys.argv[1] if len(sys.argv) > 1 else "."
    LOG_ROOT = os.environ.get("LOG_ROOT", "")
    OUT = Path(os.environ.get("OUT", "hp-drupal-core-cve-2026-9082-kev-scope"))
    SINCE = "2026-05-26T00:00:00Z"
    UNTIL = "2026-05-26T23:59:59Z"
    
    PACKAGES = [
    ]
    VERSIONS = [
    ]
    FILES = [
    ]
    DOMAINS = [
      "www.cisa.gov",
      "www.drupal.org",
      "nvd.nist.gov",
    ]
    URLS = [
      "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json",
      "https://www.drupal.org/sa-core-2026-004",
      "https://nvd.nist.gov/vuln/detail/CVE-2026-9082",
    ]
    IPS = [
    ]
    HASHES = [
    ]
    PROCESS_PATTERNS = [
    ]
    NETWORK_PATTERNS = [
    ]
    
    # Positive signal: repository, lockfile, artifact, process, or network telemetry contains one of the exact incident selectors above.
    # Escalation: any match tied to a production build, CI run, deployed asset, or secret-bearing host moves the asset to presumed exposed.
    
    OUT.mkdir(parents=True, exist_ok=True)
    indicators_file = OUT / "indicators.txt"
    
    # Collect unique indicators
    indicators = set()
    for group in [PACKAGES, VERSIONS, FILES, DOMAINS, URLS, IPS, HASHES, PROCESS_PATTERNS, NETWORK_PATTERNS]:
        for val in group:
            if val:
                indicators.add(val)
    
    with open(indicators_file, "w") as f:
        for ind in sorted(indicators):
            f.write(ind + "\n")
    
    print(f"[+] Written unique selectors to {indicators_file}")
    
    # Walk local directory
    print(f"[+] Scanning directory: {ROOT} for selectors...")
    matches = []
    exclude_dirs = {"node_modules", "vendor", "dist", ".git"}
    for root, dirs, filenames in os.walk(ROOT):
        dirs[:] = [d for d in dirs if d not in exclude_dirs]
        for filename in filenames:
            filepath = Path(root) / filename
            try:
                content = filepath.read_text(errors="ignore")
                for ind in indicators:
                    if ind in content:
                        matches.append(f"{filepath}: found '{ind}'")
            except Exception:
                pass
    
    if matches:
        (OUT / "repository-indicator-matches.txt").write_text("\n".join(matches) + "\n")
        print(f"[!] Found {len(matches)} matches in codebase!")
    
    # Optional Log Scanning
    if LOG_ROOT and os.path.exists(LOG_ROOT):
        print(f"[+] Scanning telemetry log directory: {LOG_ROOT}...")
        log_matches = []
        for root, _, filenames in os.walk(LOG_ROOT):
            for filename in filenames:
                filepath = Path(root) / filename
                try:
                    content = filepath.read_text(errors="ignore")
                    for ind in indicators:
                        if ind in content:
                            log_matches.append(f"{filepath}: found '{ind}'")
                except Exception:
                    pass
        if log_matches:
            (OUT / "exported-telemetry-indicator-matches.txt").write_text("\n".join(log_matches) + "\n")
            print(f"[!] Found {len(log_matches)} matches in logs!")
    
        if PACKAGES:
            registry_dir = OUT / "registry"
            registry_dir.mkdir(exist_ok=True)
    
    print(f"[+] Wrote scope artifacts under {OUT}")

    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

    Script: GitHub organization run, release, secret, and workflow audit

    #!/usr/bin/env python3
    import os
    import sys
    import json
    import subprocess
    from pathlib import Path
    
    if "ORG" not in os.environ:
        print("ERROR: Set ORG environment variable to the GitHub organization to audit", file=sys.stderr)
        sys.exit(1)
    
    ORG = os.environ["ORG"]
    SINCE = "2026-05-26T00:00:00Z"
    UNTIL = "2026-05-26T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-drupal-core-cve-2026-9082-kev-github-audit"))
    
    SELECTORS = [
      "www.cisa.gov",
      "www.drupal.org",
      "nvd.nist.gov",
      "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json",
      "https://www.drupal.org/sa-core-2026-004",
      "https://nvd.nist.gov/vuln/detail/CVE-2026-9082",
    ]
    
    # Positive signal: a workflow run, release, secret, key, package, or workflow change overlaps the exposure window and references an incident selector.
    # Remediation trigger: unauthorized post-exposure write activity or a secret-bearing run matching an incident selector requires token revocation and downstream cloud/registry review.
    
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "runs").mkdir(exist_ok=True)
    (OUT / "logs").mkdir(exist_ok=True)
    (OUT / "repos").mkdir(exist_ok=True)
    
    # 1. Write incident-selectors file
    selectors_file = OUT / "incident-selectors.txt"
    with open(selectors_file, "w") as sf:
        for s in SELECTORS:
            if s:
                sf.write(s + "\n")
    
    # 2. Get list of repos
    print(f"[+] Fetching repositories for organization: {ORG}")
    repo_res = subprocess.run(["gh", "repo", "list", ORG, "--limit", "1000", "--json", "nameWithOwner"], capture_output=True, text=True)
    if repo_res.returncode != 0:
        print(f"[-] Failed to fetch repos: {repo_res.stderr}", file=sys.stderr)
        sys.exit(1)
    
    repos = [r["nameWithOwner"] for r in json.loads(repo_res.stdout)]
    
    for repo in repos:
        safe_repo = repo.replace("/", "__")
        print(f"[+] Auditing repository: {repo}")
    
        # Check runs in the window
        runs_res = subprocess.run([
            "gh", "api", f"/repos/{repo}/actions/runs",
            "-f", "per_page=100",
            "-f", f"created=>={SINCE}",
            "--paginate"
        ], capture_output=True, text=True)
    
        if runs_res.returncode == 0:
            try:
                all_runs = json.loads(runs_res.stdout).get("workflow_runs", [])
                filtered_runs = [r for r in all_runs if r["created_at"] <= UNTIL]
    
                if filtered_runs:
                    with open(OUT / "runs" / f"{safe_repo}-runs.jsonl", "w") as rf:
                        for run in filtered_runs:
                            rf.write(json.dumps(run) + "\n")
    
                            # Fetch log dynamically
                            run_id = str(run["id"])
                            log_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--log"], capture_output=True, text=True)
                            if log_res.returncode == 0:
                                (OUT / "logs" / f"{safe_repo}-{run_id}.log").write_text(log_res.stdout)
    
                            # Fetch details
                            view_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--json", "databaseId,workflowName,headSha,event,createdAt,jobs"], capture_output=True, text=True)
                            if view_res.returncode == 0:
                                (OUT / "runs" / f"{safe_repo}-{run_id}.json").write_text(view_res.stdout)
            except Exception as e:
                print(f"[-] Error parsing runs for {repo}: {e}")
    
        # Check releases in window
        subprocess.run(["gh", "api", f"/repos/{repo}/releases", "-f", "per_page=100", "--paginate"], capture_output=True)
        # Check repo secrets updated in window
        subprocess.run(["gh", "api", f"/repos/{repo}/actions/secrets", "-f", "per_page=100", "--paginate"], capture_output=True)
        # Check deploy keys
        subprocess.run(["gh", "api", f"/repos/{repo}/keys", "-f", "per_page=100", "--paginate"], capture_output=True)
    
    # Scan output directory for any indicator selector matches
    print("[+] Scanning gathered telemetry for indicator matches...")
    subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(selectors_file), str(OUT)], capture_output=False)
    
    print(f"[+] Wrote GitHub audit artifacts under {OUT}")

    Script: cloud OIDC and deployment credential follow-on audit

    #!/usr/bin/env python3
    import os
    import json
    import subprocess
    from pathlib import Path
    
    SINCE = "2026-05-26T00:00:00Z"
    UNTIL = "2026-05-26T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-drupal-core-cve-2026-9082-kev-cloud-audit"))
    AWS_REGIONS = os.environ.get("AWS_REGIONS", "us-east-1").split(",")
    
    # Positive signal: token exchange or privileged write activity occurs in the exposure window from GitHub, CI/CD, package registry, or deployment automation identity.
    # Remediation trigger: unexpected write, deploy, IAM, secret, or registry activity tied to an exposed CI/CD path requires trust-policy disablement and credential rotation.
    
    OUT.mkdir(parents=True, exist_ok=True)
    
    # 1. AWS CloudTrail Audit
    print("[+] Querying AWS CloudTrail for Web Identity token exchanges...")
    aws_events = []
    for region in AWS_REGIONS:
        res = subprocess.run([
            "aws", "cloudtrail", "lookup-events",
            "--region", region,
            "--start-time", SINCE,
            "--end-time", UNTIL,
            "--lookup-attributes", "AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity",
            "--output", "json"
        ], capture_output=True, text=True)
    
        if res.returncode == 0:
            try:
                events = json.loads(res.stdout).get("Events", [])
                for event in events:
                    ct = json.loads(event.get("CloudTrailEvent", "{}"))
                    ct["region"] = region
                    aws_events.append(ct)
            except Exception as e:
                print(f"[-] Error parsing AWS CloudTrail events: {e}")
    
    if aws_events:
        with open(OUT / "aws-assume-role-with-web-identity.jsonl", "w") as f:
            for ev in aws_events:
                f.write(json.dumps(ev) + "\n")
    
        # Audit follow-on events for returned access keys
        for ev in aws_events:
            access_key = ev.get("responseElements", {}).get("credentials", {}).get("accessKeyId")
            region = ev.get("region", "us-east-1")
            if access_key:
                print(f"[+] Enumerating AWS events for AccessKey: {access_key}")
                f_res = subprocess.run([
                    "aws", "cloudtrail", "lookup-events",
                    "--region", region,
                    "--start-time", SINCE,
                    "--end-time", UNTIL,
                    "--lookup-attributes", f"AttributeKey=AccessKeyId,AttributeValue={access_key}",
                    "--output", "json"
                ], capture_output=True, text=True)
                if f_res.returncode == 0:
                    try:
                        f_events = json.loads(f_res.stdout).get("Events", [])
                        with open(OUT / "aws-follow-on-api-calls.jsonl", "a") as ff:
                            for fe in f_events:
                                ff.write(fe.get("CloudTrailEvent", "{}") + "\n")
                    except Exception as e:
                        print(f"[-] Error writing follow-on events: {e}")
    
    # 2. Azure Activity Log Audit
    print("[+] Querying Azure activity logs...")
    az_res = subprocess.run([
        "az", "monitor", "activity-log", "list",
        "--start-time", SINCE,
        "--end-time", UNTIL,
        "--query", "[?contains(operationName.value, 'write') || contains(operationName.value, 'delete') || contains(operationName.value, 'Microsoft.ManagedIdentity')]",
        "-o", "json"
    ], capture_output=True, text=True)
    
    if az_res.returncode == 0:
        (OUT / "azure-write-delete-activity.json").write_text(az_res.stdout)
    
    # 3. GCP Logging Audit
    print("[+] Querying GCP Cloud Logging...")
    gcp_filter = f'timestamp>="{SINCE}" AND timestamp<="{UNTIL}" AND (protoPayload.methodName="google.sts.v1.SecurityTokenService.ExchangeToken" OR protoPayload.methodName:"GenerateAccessToken" OR protoPayload.methodName:"CreateServiceAccountKey" OR protoPayload.methodName:"SetIamPolicy")'
    gcp_res = subprocess.run([
        "gcloud", "logging", "read", gcp_filter,
        "--format", "json"
    ], capture_output=True, text=True)
    
    if gcp_res.returncode == 0:
        (OUT / "gcp-token-and-iam-activity.json").write_text(gcp_res.stdout)
    
    print(f"[+] Wrote cloud audit artifacts under {OUT}")

    Script: registry metadata and artifact audit

    #!/usr/bin/env python3
    import os
    import json
    import subprocess
    from pathlib import Path
    
    SINCE = "2026-05-26T00:00:00Z"
    OUT = Path(os.environ.get("OUT", "hp-drupal-core-cve-2026-9082-kev-registry-audit"))
    PACKAGES = [
    ]
    VERSIONS = [
    ]
    
    # Positive signal: workflow files or extensions reference the affected action/extension names or versions.
    # Remediation trigger: exposed secrets or OIDC federation policies must be immediately rotated.
    
    OUT.mkdir(parents=True, exist_ok=True)
    
    with open(OUT / "affected-versions.txt", "w") as av:
        for version in VERSIONS:
            if version:
                av.write(version + "\n")
    
        # 1. Search local workspace files for the affected actions/extensions
        print("[+] Scanning workspace workflows for selectors...")
        for file in Path(".").glob(".github/workflows/**/*.yml"):
            subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), str(file)])
    
        # 2. HOW TO ROTATE EXPOSED GITHUB ACTIONS SECRETS:
        # Overwrite compromised secrets with newly generated credentials:
        # subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--body", "my-new-secret-value", "--repo", "my-org/my-repo"])
        # For organization-level secrets:
        # subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--org", "my-org", "--visibility", "private"])
        # Revoke compromised OIDC federated trust credentials in AWS/GCP and redeploy the IAM trust policy:
        # subprocess.run(["aws", "iam", "update-assume-role-policy", "--role-name", "my-role-name", "--policy-document", "file://new-clean-trust-policy.json"])
    
    print(f"[+] Wrote registry audit artifacts under {OUT}")

    Sources

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