MuntashirAkon/AppManager · critical · IOException

Zip slip vulnerability detected! Expected dest: ${new File(r

Error message

Zip slip vulnerability detected!
Expected dest: ${new File(realDestPath, entry.getName())}
Actual path: ${filename != null ? new File(realDestPath, filename) : realDestPath}

What it means

TarUtils.extract performs an early zip-slip check on every tar entry: entry names are normalized (Paths.normalize) and rejected if normalization returns null or the result still starts with '../'. Throwing IOException("Zip slip vulnerability detected!...") prevents creating any files for an entry whose name would escape the destination directory via path traversal. The message contrasts the expected destination with the actual (traversal) path. This is a protective security error, not a bug in the caller's code.

Source

Thrown at app/src/main/java/io/github/muntashirakon/AppManager/utils/TarUtils.java:158

        Pattern[] exclusionPatterns;
        if (exclusions != null) {
            exclusionPatterns = new Pattern[exclusions.length];
            for (int i = 0; i < exclusions.length; ++i) {
                exclusionPatterns[i] = Pattern.compile(exclusions[i]);
            }
        } else exclusionPatterns = null;
        // Run extraction
        try (SplitInputStream sis = new SplitInputStream(sources);
             BufferedInputStream bis = new BufferedInputStream(sis);
             InputStream is = createDecompressedStream(bis, type)) {
            try (TarArchiveInputStream tis = new TarArchiveInputStream(is)) {
                String realDestPath = dest.getRealFilePath();
                TarArchiveEntry entry;
                while ((entry = tis.getNextEntry()) != null) {
                    String filename = Paths.normalize(entry.getName());
                    // Early zip slip vulnerability check to avoid creating any files at all
                    if (filename == null || filename.startsWith("../")) {
                        throw new IOException("Zip slip vulnerability detected!" +
                                "\nExpected dest: " + new File(realDestPath, entry.getName()) +
                                "\nActual path: " + (filename != null ? new File(realDestPath, filename) : realDestPath));
                    }
                    Path file;
                    if (entry.isDirectory()) {
                        file = dest.createDirectoriesIfRequired(filename);
                    } else file = dest.createNewArbitraryFile(filename, null);
                    if (!entry.isDirectory() && (!Paths.isUnderFilter(file, dest, filterPatterns)
                            || Paths.willExclude(file, dest, exclusionPatterns))) {
                        // Unlike create, there's no efficient way to detect if a directory contains any filters.
                        // Therefore, directory can't be filtered during extraction
                        file.delete();
                        continue;
                    }
                    // Check if the given entry is a link.
                    if (entry.isSymbolicLink() && file.getFilePath() != null) {
                        if ((!Paths.isUnderFilter(file, dest, filterPatterns) || Paths.willExclude(file, dest, exclusionPatterns))) {
                            // Do not create this link even if it is a directory

View on GitHub (pinned to 0152f468fc)

Solutions

  1. Do not extract the archive if it comes from an untrusted source; reject it and inform the user
  2. Sanitize/rebuild entry names server-side before archiving (strip leading '/' and '..' segments)
  3. Inspect the archive listing first (list entries without extracting) and reject any with '..' or absolute paths
  4. Catch IOException and surface the message — it identifies the offending entry name

Example fix

// before
TarUtils.extract(input, destDir, ...); // throws on malicious entry
// after
try {
    TarUtils.extract(input, destDir, ...);
} catch (IOException e) {
    if (e.getMessage().contains("Zip slip")) {
        throw new SecurityException("Archive rejected: " + e.getMessage(), e);
    }
    throw e;
}
Defensive patterns

Strategy: validation

Validate before calling

for (String entryName : archiveEntryNames) {
    String normalized = Paths.normalize(entryName);
    if (normalized == null || normalized.startsWith("../") || entryName.startsWith("/")) {
        throw new SecurityException("Unsafe entry: " + entryName);
    }
}

Type guard

boolean isSafeEntryName(String name) {
    String n = Paths.normalize(name);
    return n != null && !n.startsWith("../") && !n.startsWith("/");
}

Try / catch

try {
    TarUtils.extract(in, dest, filters);
} catch (IOException e) {
    if (e.getMessage().contains("Zip slip")) {
        throw new SecurityException("Path traversal blocked: " + e.getMessage(), e);
    }
    throw e;
}

Prevention

When it happens

Trigger: Extracting a tar/zip archive containing entries with names like '../../etc/passwd', absolute paths, or names that after symlink/parent normalization leave the destination directory (dest.getRealFilePath() prefix check fails).

Common situations: Processing untrusted or attacker-supplied archives (downloaded backups, shared tarballs); archives crafted by a malicious packager; archives produced on other systems with '..' segments; symlink entries inside the archive pointing outside the destination (caught later at 419).

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of MuntashirAkon/AppManager@0152f468fc (2026-09-12). Data as JSON: /api/errors/7f2c51bb8a3579bb. Report an issue: GitHub.