GHSA-j6hm-v3x2-qv6j: land.oras:oras-java-sdk: Symlink-based path traversal in ArchiveUtils.untar / unzip allows arbitrary file write outside extraction directory
Summary`ArchiveUtils.untar(InputStream, Path)` and `ArchiveUtils.unzip(InputStream, Path)` in `land.oras:oras-java-sdk` create symbolic-link entries from an archive without validating the symlink target. A malicious tar (or zip) shipped as an OCI layer blob can place a symlink under the extraction directory whose target points outside that directory, then ship a regular-file entry whose path traverses through that symlink. The subsequent `Files.newOutputStream` call follows the symlink and writes the file outside the caller's chosen `target` directory.This bypasses the existing `ensureSafeEntry` containment check, which only validates `entry.getName()` and not `entry.getLinkName()`. Reachable from the public `Registry.pullArtifact` / `OCILayout.pullArtifact` API whenever the manifest layer carries `io.deis.oras.content.unpack=true`, and from any direct caller of `ArchiveUtils.untar` / `ArchiveUtils.uncompressuntar` (e.g. consumers that pull an archive blob and extract it themselves, such as project-scaffolding CLIs that fetch templates from OCI registries).Affected versions`land.oras:oras-java-sdk` `<= 0.6.3` (current `main` commit `8e39dd54f0b9e981a5f246aefb851298187578e5`, file `src/main/java/land/oras/utils/ArchiveUtils.java` lines 400-443 for `untar`, lines 350-393 for `unzip`).Earlier path-traversal fix in PR #703 (released as `0.6.2`) addressed the `org.opencontainers.image.title` annotation sink (GHSA-xm96-gfjx-jcrc), but did not extend containment to symlink targets inside archive entries.Privilege requiredNetwork-position: any party able to serve an OCI artifact / archive blob that the victim's `oras-java-sdk`-based tool pulls and extracts. This includes:A compromised or attacker-operated OCI registry endpoint (typo-squat, MITM, supply-chain into a public registry namespace).A malicious upstream artifact author who pushes a single tampered blob to a registry the victim trusts.No registry authentication needed on the victim side beyond what is required to pull the artifact; the attack is fully driven by the contents of the layer the victim downloads.Root cause`src/main/java/land/oras/utils/ArchiveUtils.java` `untar(InputStream, Path)`, lines 400-443:`ensureSafeEntry` (lines 261-268) only normalizes `entry.getName()` against `target`. `entry.getLinkName()` for symlink entries is passed straight to `createSymbolicLink`. The subsequent file-write code path uses `Files.newOutputStream` without `LinkOption.NOFOLLOW_LINKS`, so when a later entry's path traverses through the previously-created symlink, the write resolves the symlink and lands outside `target`.The same primitive applies to `unzip(InputStream, Path)` at lines 350-393 (symlink path at line 379 uses `createSymbolicLink(outputPath, Paths.get(linkStr))` without target validation).Reachability into the public API:`Registry.pullArtifact` -> `pullLayer` (Registry.java:1088). When `io.deis.oras.content.unpack=true` is set on the layer, `pullLayer` calls `ArchiveUtils.untar(Files.newInputStream(tempArchive.getPath()), path)` at line 1109. The attacker-controlled blob is the input.`OCILayout.pullArtifact` similar path through the layout's blob store.Any direct caller of `ArchiveUtils.untar` / `ArchiveUtils.uncompressuntar` / `ArchiveUtils.unzip` from a downstream consumer that fetches an archive blob and extracts it.Proof of concept (E2E against deployed Maven Central `land.oras:oras-java-sdk:0.6.3`)`pom.xml`:`src/main/java/TarSlipPoc.java`:Run transcript (JDK 25, Apache Maven 3.9.16, `oras-java-sdk:0.6.3` from Maven Central):Negative control: same malicious tar, extracted through a routine that resolves `entry.getLinkName()` against the entry's parent and requires the result to stay under `target`, AND opens regular files with `LinkOption.NOFOLLOW_LINKS`:ImpactArbitrary file write under the privileges of the JVM process that runs the SDK. Files in any directory the process can write to are overwritten, including build outputs, CI workspace siblings, `~/.bashrc`-style profile files when extraction happens under the user's home, and `~/.docker/config.json` / `~/.config/containers/auth.json` to plant attacker registry credentials.Indirect code execution when the overwritten target is a shell init file, a cron drop-in, a systemd unit drop-in, an SSH `authorized_keys` (when extraction happens under a service account home), a Jenkins / Tekton task runner script, or any executable invoked by the surrounding pipeline.Supply-chain pivot: any oras-java-sdk consumer that pulls an artifact and extracts it (project-scaffolding CLIs, Helm-OCI fetchers, plugin pull tools, CNCF artifact handlers) becomes a remote-attacker write primitive.CVSS 3.1 vector: `AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H` (~8.6 HIGH). The integrity impact is high because the attacker fully chooses both the link target path and the file payload; availability is high because critical files (cron, init scripts, application binaries on the CI worker) can be overwritten.Suggested fixTwo complementary changes in `src/main/java/land/oras/utils/ArchiveUtils.java`. Both are required: the link-target check stops the symlink from being created in the first place; `LinkOption.NOFOLLOW_LINKS` is the second line of defense against any path that already contains a symlink (race, pre-existing target dir, etc.).(1) Validate symlink targets in `untar` and `unzip` against the normalized extraction root, paralleling the existing `ensureSafeEntry` shape:Call this immediately before each `createSymbolicLink(...)` site (one in `untar`, one in `unzip`).(2) Open regular-file writes with `LinkOption.NOFOLLOW_LINKS` so a pre-existing symlink under the extraction path cannot be silently followed:`CREATE_NEW` (instead of the implicit `CREATE` + `TRUNCATE_EXISTING`) guarantees the open fails if the path resolves to an existing entry (symlink or regular file), and is consistent with archive extraction semantics where layers should not silently clobber siblings.A regression test that builds the malicious tar inline and asserts the post-extract state stays within `target` is the natural shape for `src/test/java/land/oras/utils/ArchiveUtilsTest.java`.CreditReported by tonghuaroot.
CVSS v3.1: 0