GHSA-P634-W6R4-RJP2

Vulnerability from github – Published: 2026-09-29 23:11 – Updated: 2026-09-29 23:11
VLAI
Summary
adm-zip: Duplicate ZIP entry names: getEntry() and extractAllTo() resolve to different content
Details

Summary

A ZIP file can contain two entries with the identical name. adm-zip keeps both in its internal entry list, but its name-lookup table only retains the last one written. getEntry(name) and extractAllTo() walk these two different internal structures, so they can each resolve a duplicate name to a different entry. An application that validates a named entry's contents via getEntry() before trusting an archive, then extracts the whole archive, can end up approving one file's content while a different file's bytes are what actually land on disk under that name.

Details

  • zipFile.js:58-83 retains both entries in entryList but overwrites entryTable[name] with only the last one written.
  • adm-zip.js:83-95,658-663 uses entryTable for getEntry() lookups — returns the last duplicate.
  • adm-zip.js:769-914 iterates entryList for extraction — writes the first duplicate (sync, default overwrite policy).

PoC

const AdmZip = require('adm-zip');
const z = new AdmZip({ noSort: true });
z.addFile('a.txt', Buffer.from('FIRST'));
z.addFile('b.txt', Buffer.from('SECOND'));
const raw = Buffer.from(z.toBuffer());
// rename the a.txt entry to b.txt directly in the raw bytes
for (let at = raw.indexOf('a.txt'); at >= 0; at = raw.indexOf('a.txt', at + 5)) {
  raw.write('b.txt', at);
}
const parsed = new AdmZip(raw, { noSort: true });
const validated = parsed.getEntry('b.txt').getData().toString();
parsed.extractAllTo(outDir, false);
// validated === "SECOND", but the file written to disk === "FIRST"

Reproduced on the pinned commit (2b4d84087d45344643e0183756e19191d52815cc)

Impact

An application that checks a named entry's content before trusting an untrusted ZIP, then extracts it, can be made to approve different bytes than what actually gets written to disk — the classic check/use split that this kind of validate-then-extract pattern relies on.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.6.0"
      },
      "package": {
        "ecosystem": "npm",
        "name": "adm-zip"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.6.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-436",
      "CWE-696"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-29T23:11:01Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Summary\n\nA ZIP file can contain two entries with the identical name. adm-zip keeps both in its internal entry list, but its name-lookup table only retains the last one written. `getEntry(name)` and `extractAllTo()` walk these two different internal structures, so they can each resolve a duplicate name to a *different* entry. An application that validates a named entry\u0027s contents via `getEntry()` before trusting an archive, then extracts the whole archive, can end up approving one file\u0027s content while a different file\u0027s bytes are what actually land on disk under that name.\n\n### Details\n- `zipFile.js:58-83` retains both entries in `entryList` but overwrites `entryTable[name]` with only the last one written.\n- `adm-zip.js:83-95,658-663` uses `entryTable` for `getEntry()` lookups \u2014 returns the *last* duplicate.\n- `adm-zip.js:769-914` iterates `entryList` for extraction \u2014 writes the *first* duplicate (sync, default overwrite policy).\n\n\n### PoC\n```js\nconst AdmZip = require(\u0027adm-zip\u0027);\nconst z = new AdmZip({ noSort: true });\nz.addFile(\u0027a.txt\u0027, Buffer.from(\u0027FIRST\u0027));\nz.addFile(\u0027b.txt\u0027, Buffer.from(\u0027SECOND\u0027));\nconst raw = Buffer.from(z.toBuffer());\n// rename the a.txt entry to b.txt directly in the raw bytes\nfor (let at = raw.indexOf(\u0027a.txt\u0027); at \u003e= 0; at = raw.indexOf(\u0027a.txt\u0027, at + 5)) {\n  raw.write(\u0027b.txt\u0027, at);\n}\nconst parsed = new AdmZip(raw, { noSort: true });\nconst validated = parsed.getEntry(\u0027b.txt\u0027).getData().toString();\nparsed.extractAllTo(outDir, false);\n// validated === \"SECOND\", but the file written to disk === \"FIRST\"\n```\n\nReproduced on the pinned commit (`2b4d84087d45344643e0183756e19191d52815cc`)\n\n### Impact\nAn application that checks a named entry\u0027s content before trusting an untrusted ZIP, then extracts it, can be made to approve different bytes than what actually gets written to disk \u2014 the classic check/use split that this kind of validate-then-extract pattern relies on.",
  "id": "GHSA-p634-w6r4-rjp2",
  "modified": "2026-09-29T23:11:01Z",
  "published": "2026-09-29T23:11:01Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/cthackers/adm-zip/security/advisories/GHSA-p634-w6r4-rjp2"
    },
    {
      "type": "WEB",
      "url": "https://github.com/cthackers/adm-zip/commit/05101d47b3b983b705cc3e66fc34366118ba7b99"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/cthackers/adm-zip"
    },
    {
      "type": "WEB",
      "url": "https://github.com/cthackers/adm-zip/releases/tag/v0.6.1"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "adm-zip: Duplicate ZIP entry names: getEntry() and extractAllTo() resolve to different content"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…