← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2025-6965 · CWE-197 · Disclosed 2025-07-15

There exists a vulnerability in SQLite versions before 3

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

This is less an open front door than a cracked gearbox that only fails after you already hijack the controls

CVE-2025-6965 is an integer truncation bug in SQLite before 3.50.2 that can corrupt memory when SQLite processes an aggregate query with an extreme number of distinct column references. Upstream fixed it on 2025-06-27 and shipped 3.50.2 on 2025-06-28, changing the engine to error out early when aggregate terms exceed the maximum column count. The practical precondition is the real story: the attacker needs an application that lets them feed SQLite hostile SQL directly, or in some cases a hostile SQLite database file that the app will open and query.

The vendor 9.8/CRITICAL label is inflated for enterprise patch triage. SQLite is a library, not a remotely reachable service, and SQLite’s own security guidance is blunt that most reported SQLite CVEs assume arbitrary SQL execution or malicious database-file handling first. Add the exploit-specific friction here — a very large aggregate query, often beyond sane production query shapes and sometimes beyond default SQLite limits — and this stops looking like internet-scale unauthenticated RCE and starts looking like a post-input-control memory corruption edge case.

"Critical on paper, but real exploitation usually starts with already-owning SQL input."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Gain control of SQLite input

The attacker first needs an application path that hands untrusted content to SQLite. In practice that means SQL injection into app-generated SQLite queries, an intentional untrusted-query feature, or an app that opens attacker-supplied .db content and runs queries over it. Weaponized tool/ref: generic SQLi tooling such as sqlmap, or a malicious crafted database delivered through app workflow.
Conditions required:
  • Application embeds or links SQLite
  • Attacker can supply arbitrary SQL text or a malicious SQLite database file
  • Target application actually executes queries over that attacker-controlled input
Where this breaks in practice:
  • SQLite is usually embedded and not directly network-exposed
  • Most enterprise apps do not intentionally allow raw remote SQL execution
  • If the path is via malicious database file, the victim app must open and query that file
Detection/coverage: SCA and package scanners catch version exposure well; runtime exploit detection is weak because this looks like app-level SQL handling, not a distinct network signature.
STEP 02

Build the oversized aggregate query

The exploit path requires an aggregate query with a *very* large number of distinct column references. Google’s technical write-up, echoed on oss-security, says the bad state appears when the aggregate context processes more than 32,767 columns and a 32-bit index truncates into a signed 16-bit value. Weaponized tool/ref: custom PoC SQL from the Google advisory path, not commodity exploit kits.
Conditions required:
  • Attacker can shape the exact SQL statement
  • The application/runtime does not clamp SQLite limits low enough to stop pathological query size
  • The query reaches the vulnerable code path during prepare/codegen
Where this breaks in practice:
  • This is not a normal business query shape
  • SQLite default column limits are much lower than the hard maximum in many builds
  • Apps that use sqlite3_limit() defensively can cut this off before exploitation
Detection/coverage: DB telemetry may show abnormally large SELECT statements if the app logs SQL, but many embedded SQLite deployments log nothing useful.
STEP 03

Trigger truncation and out-of-bounds access

On vulnerable builds, the oversized aggregate bookkeeping can truncate an index and later use it for out-of-bounds array access. In debug builds this tends to crash on assertions; in non-debug builds the bug can produce out-of-bounds reads and writes, which is where memory corruption enters the picture. Weaponized tool/ref: custom crash/heap-corruption harnesses plus ASAN-enabled repros.
Conditions required:
  • SQLite version is below 3.50.2 or not distro-backported
  • The vulnerable code path is compiled in and reachable
  • The crafted query survives any app-level validation
Where this breaks in practice:
  • Many observed outcomes are crashes rather than stable code execution
  • Allocator behavior, compile flags, and host process hardening matter a lot
  • Reproducible exploitation is materially harder than causing a fault
Detection/coverage: Crash telemetry, ASAN, EDR memory-corruption signals, and abnormal process termination are more realistic than signature-based IDS here.
STEP 04

Convert corruption into impact

Impact ranges from process crash to potential code execution in the host process, depending on how the target app uses SQLite and what mitigations exist around it. This is the only step where the CVSS C/I/A:H story becomes plausible, but it arrives *after* the attacker already clears several narrow prerequisites. Weaponized tool/ref: bespoke exploit development against the host application, not just SQLite alone.
Conditions required:
  • Corruption is controllable enough to steer execution or expose sensitive memory
  • Target process has meaningful privileges or access to sensitive data
  • Exploit mitigations in the host process do not stop the chain
Where this breaks in practice:
  • Blast radius is usually the embedding process, not the whole fleet
  • Modern exploit mitigations and sandboxing reduce reliability
  • An attacker who already has SQL injection may have easier paths to app-layer abuse than memory-corruption chaining
Detection/coverage: EDR may catch child-process spawn, memory abuse, or crash loops after corruption, but there is little pre-exploit network detection value.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo confirmed KEV entry and no public broad exploitation campaign identified. Google said the flaw was *known to threat actors* and *at risk of being exploited*, which is notable, but that is not the same as widespread observed exploitation.
Proof-of-concept availabilityTechnical details are public. Google Security Research published a GHSA-backed analysis path referenced from oss-security, including the vulnerable function and the >32,767 aggregate-column condition. I did not find a widely adopted commodity exploit module.
EPSS0.01617 from the user-provided intel block. That is a low exploit-likelihood signal relative to what you'd expect for a true internet-scale unauthenticated RCE.
KEV statusNot listed in CISA KEV per the current user-provided intel, and no CISA CVE-specific entry was surfaced in source review.
CVSS reality checkVendor/NVD CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H assumes a remotely reachable target with no prior foothold. That does not match how SQLite is normally deployed. SUSE rescored it to 7.7 with AC:H/PR:L, which is much closer to operational reality.
Affected versionsUpstream affected: SQLite < 3.50.2. The vulnerable behavior is in aggregate-query handling, not in a network listener, because SQLite is an embedded C library.
Fixed versionsUpstream fix: 3.50.2. Official distro backports exist: Ubuntu fixes include 22.04 LTS: 3.37.2-2ubuntu0.5, 24.04 LTS: 3.45.1-1ubuntu2.4; Debian fixes include bookworm: 3.40.1-2+deb12u2, trixie: 3.46.1-7+deb13u1.
Exposure/scanning realityExpect scanner noise. Package/SBOM tools will flag embedded SQLite everywhere because SQLite is bundled into countless apps, but Shodan/Censys-style internet exposure is the wrong lens here because SQLite is not a standalone network service.
Disclosure and reportingPublished: 2025-07-15 in CVE/NVD. Credits: the CVE record names *Vlad Stolyarov of Google Threat Analysis Group, with assistance from Google Big Sleep*.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to MEDIUM (5.6/10)

The decisive downgrade factor is attacker position: this bug almost always requires prior control over SQL input or a malicious SQLite file path, which sharply limits reachable population. The extra exploit-specific friction — an extreme aggregate-query shape tied to very high column counts — keeps this out of the HIGH bucket for generic enterprise fleets despite the memory-corruption impact.

HIGH Prerequisite assessment: most real exploit paths require arbitrary SQL execution or hostile DB-file handling first
MEDIUM Fleetwide exposure assessment: scanners will overstate risk because SQLite is bundled broadly
MEDIUM Impact assessment: memory corruption is real, but stable RCE reliability varies heavily by host process and build

Why this verdict

  • Requires prior input control: the attacker generally needs unaudited SQL execution or an app that opens attacker-supplied SQLite files, which implies a prior compromise or a very specific app design.
  • Reachable population is narrow: SQLite is a library, not a listening service, so the vendor's AV:N/PR:N story badly overstates generic enterprise reachability.
  • Exploit path is pathological: public technical detail points to an aggregate-query shape involving more than 32,767 processed columns, far outside normal application behavior and often blocked by default or reduced limits.
  • No KEV / low EPSS: there is no confirmed KEV listing and the supplied EPSS is low, which is not how internet-wormable library bugs usually look.
  • Still not low: Google stated the issue was known to threat actors and at risk of exploitation, and the end state is memory corruption in the embedding process, not just a cosmetic fault.

Why not higher?

I am not calling this HIGH or CRITICAL because the attack chain is not direct-to-host. It compounds multiple prerequisites: an app must embed vulnerable SQLite, expose attacker influence over SQL or DB content, allow an extreme query shape, and then survive the jump from crash to useful corruption. That is a very different risk profile from a remotely reachable service bug.

Why not lower?

I am not dropping it to LOW because this is still real memory corruption with a credible path to process compromise in some applications. The Google intelligence note that the flaw was known to threat actors adds enough threat context that defenders should not dismiss it as purely theoretical.

05 · Compensating Control

What to do — in priority order.

  1. Clamp SQLite limits — Lower SQLITE_LIMIT_COLUMN with sqlite3_limit() for any component that may process untrusted SQL or third-party content. There is no noisgate mitigation SLA for MEDIUM, so apply this first where the exposure pattern exists while you work inside the remediation window.
  2. Disable trusted schema — Set SQLITE_DBCONFIG_TRUSTED_SCHEMA=0 or PRAGMA trusted_schema=OFF on each connection in apps that touch untrusted databases. This reduces abuse of application-defined functions and virtual tables; for a MEDIUM finding, there is no mitigation SLA — go straight to the 365-day remediation window, but high-risk consumers should harden sooner.
  3. Turn on defensive mode — Use SQLITE_DBCONFIG_DEFENSIVE in products that accept untrusted input. It will not patch this bug directly, but it narrows attack surface and is explicitly recommended by upstream for hostile-input cases; with MEDIUM, deploy on exposed apps during normal engineering work rather than treating it like an emergency change.
  4. Segregate untrusted SQLite workloads — Browser-like, sync, import, and plugin-driven features that open third-party SQLite files should run with least privilege and, where possible, sandboxing. That does more to cap blast radius than blanket fleet panic over every embedded copy.
What doesn't work
  • A WAF alone does not solve this unless the only viable entry path is classic web SQL injection and the WAF actually sees the payload. Many affected cases are local-file or app-internal parsing paths.
  • Blindly upgrading only the standalone sqlite3 CLI does not fix apps that statically bundle or privately ship their own SQLite copy.
  • Relying on the upstream version string alone can mislead on Linux because distro backports may ship a patched older-looking package version.
06 · Verification

Crowdsourced verification payload.

Run this on the target host or golden image, not your laptop, because you need the host's local SQLite runtime and package metadata. Invoke it with python3 check_cve_2025_6965.py; standard user rights are usually enough on Debian/Ubuntu, and the script checks Python's linked SQLite, the sqlite3 CLI if present, and official Debian/Ubuntu package backports before printing exactly VULNERABLE, PATCHED, or UNKNOWN.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
# check_cve_2025_6965.py
# Detect likely exposure to CVE-2025-6965 (SQLite aggregate-term integer truncation)
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN

import os
import re
import sys
import shutil
import sqlite3
import subprocess

UPSTREAM_FIXED = (3, 50, 2)
UBUNTU_FIXED = {
    'focal': '3.31.1-4ubuntu0.7+esm1',
    'jammy': '3.37.2-2ubuntu0.5',
    'noble': '3.45.1-1ubuntu2.4',
    'plucky': '3.46.1-3ubuntu0.2',
    'questing': '3.46.1-6ubuntu1',
    'resolute': '3.46.1-6ubuntu1',
}
DEBIAN_FIXED = {
    'bookworm': '3.40.1-2+deb12u2',
    'trixie': '3.46.1-7+deb13u1',
}


def run(cmd):
    try:
        p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=10)
        return p.returncode, p.stdout.strip(), p.stderr.strip()
    except Exception:
        return 999, '', ''


def parse_triplet(s):
    m = re.search(r'(\d+)\.(\d+)\.(\d+)', s or '')
    if not m:
        return None
    return tuple(int(x) for x in m.groups())


def ge_triplet(a, b):
    return a >= b


def read_os_release():
    data = {}
    try:
        with open('/etc/os-release', 'r', encoding='utf-8') as f:
            for line in f:
                line = line.strip()
                if '=' in line:
                    k, v = line.split('=', 1)
                    data[k] = v.strip().strip('"')
    except Exception:
        pass
    return data


def dpkg_installed_version(pkg):
    rc, out, _ = run(['dpkg-query', '-W', '-f=${Version}', pkg])
    return out if rc == 0 and out else None


def dpkg_ge(ver_a, ver_b):
    rc, _, _ = run(['dpkg', '--compare-versions', ver_a, 'ge', ver_b])
    return rc == 0


def check_deb_backport():
    if not shutil.which('dpkg-query') or not shutil.which('dpkg'):
        return None
    osr = read_os_release()
    distro = osr.get('ID', '').lower()
    codename = osr.get('VERSION_CODENAME', '').lower()
    pkgver = dpkg_installed_version('sqlite3') or dpkg_installed_version('libsqlite3-0')
    if not pkgver:
        return None

    if distro == 'ubuntu' and codename in UBUNTU_FIXED:
        return dpkg_ge(pkgver, UBUNTU_FIXED[codename])
    if distro == 'debian' and codename in DEBIAN_FIXED:
        return dpkg_ge(pkgver, DEBIAN_FIXED[codename])
    return None


def python_sqlite_status():
    try:
        v = parse_triplet(sqlite3.sqlite_version)
        if v is None:
            return None
        return ge_triplet(v, UPSTREAM_FIXED)
    except Exception:
        return None


def cli_sqlite_status():
    exe = shutil.which('sqlite3')
    if not exe:
        return None
    rc, out, _ = run([exe, '--version'])
    if rc != 0:
        return None
    v = parse_triplet(out)
    if v is None:
        return None
    return ge_triplet(v, UPSTREAM_FIXED)


def main():
    results = []

    deb_backport = check_deb_backport()
    if deb_backport is not None:
        results.append(deb_backport)

    py_status = python_sqlite_status()
    if py_status is not None:
        results.append(py_status)

    cli_status = cli_sqlite_status()
    if cli_status is not None:
        results.append(cli_status)

    if not results:
        print('UNKNOWN')
        sys.exit(2)

    if True in results:
        # Any verified patched signal is enough to avoid a false vulnerable call
        # on Debian/Ubuntu backports or host runtimes newer than 3.50.2.
        print('PATCHED')
        sys.exit(0)

    if all(r is False for r in results):
        print('VULNERABLE')
        sys.exit(1)

    print('UNKNOWN')
    sys.exit(2)


if __name__ == '__main__':
    main()
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning: stop treating this as a fleetwide critical fire drill just because every scanner found an embedded SQLite copy. Triage for *actual exploitability*: identify apps that accept untrusted SQL or open attacker-supplied SQLite databases, verify distro backports before raising tickets, and patch those higher-risk consumers first. For a MEDIUM noisgate rating there is no noisgate mitigation SLA — go straight to the 365-day remediation window; use the noisgate remediation SLA to get verified vulnerable instances onto the patch plan within <= 365 days, but accelerate any browser-like, import-heavy, or user-content-facing SQLite consumers into the next normal maintenance cycle.

Sources

  1. SQLite CVE status page
  2. SQLite 3.50.2 release notes
  3. SQLite fix commit 5508b56fd2
  4. NVD CVE-2025-6965
  5. SQLite security guidance
  6. SQLite limits documentation
  7. Google blog on Big Sleep and CVE-2025-6965
  8. Ubuntu CVE page with backported fixes
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.