HMCL-dev/HMCL · error · IOException

Theme-pack asset escapes the installed directory:

Error message

Theme-pack asset escapes the installed directory: 

What it means

For directory-backed theme packs, resolveInstalledAsset resolves the requested entry against the installed directory and then verifies the resolved path stays inside it (after normalization). If the entry name escapes the directory (e.g. contains ../), the manager blocks it as a path-traversal attempt and throws this IOException.

Solutions

  1. Sanitize the entry name before calling: strip '..' segments and leading separators, e.g. via ThemePackAsset.normalizeEntryName on your own inputs.
  2. Only resolve entry names that come from the pack's own manifest, never from untrusted input.
  3. If you control the pack, fix the manifest's asset paths to be pack-relative without traversal segments.

Example fix

// before
resolveInstalledAsset(location, untrustedName);
// after
String safe = ThemePackAsset.normalizeEntryName(untrustedName);
if (!safe.contains("..")) resolveInstalledAsset(location, safe);
Defensive patterns

Strategy: validation

Validate before calling

String normalized = ThemePackAsset.normalizeEntryName(entryName);
if (normalized.contains("..") || normalized.startsWith("/")) {
    throw new IllegalArgumentException("unsafe asset entry: " + entryName);
}

Type guard

static boolean isSafeEntryName(String entry) {
    String n = ThemePackAsset.normalizeEntryName(entry);
    return !n.isEmpty() && !n.contains("..") && !n.startsWith("/") && !n.contains("\\");
}

Try / catch

try {
    ThemePackManager.resolveInstalledAsset(location, entry);
} catch (IOException e) {
    if (e.getMessage().startsWith("Theme-pack asset escapes the installed directory")) {
    // reject the pack; treat as untrusted input
    }
}

Prevention

When it happens

Trigger: Calling resolveInstalledAsset() with an entryName like "../../etc/passwd" or an absolute/symlink-escaping name whose normalized resolution does not start with the installed directory.

Common situations: A malicious or buggy theme-pack manifest referencing assets outside its directory; trusting unsanitized entry names from config or a remote pack list; entry names containing '..' segments from old exports.

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 HMCL-dev/HMCL@24702dc5a0 (2026-09-10). Data as JSON: /api/errors/8b20cd3f762f5635. Report an issue: GitHub.

Appendix: source

Thrown at HMCL/src/main/java/org/jackhuang/hmcl/theme/ThemePackManager.java:1212

    /// @param entryName the theme-pack relative asset entry name
    /// @return the resolved resource
    /// @throws IOException if the asset cannot be read
    public static ThemePackResource resolveInstalledAsset(ThemePackLocation location, String entryName) throws IOException {
        Objects.requireNonNull(location);
        String normalizedEntryName = ThemePackAsset.normalizeEntryName(entryName);
        if (location instanceof ThemePackLocation.Builtin builtin) {
            return resolveBuiltinAsset(builtin, normalizedEntryName);
        }

        @Nullable Path file = location.file();
        if (file == null) {
            throw new IOException("Theme pack location has no readable file: " + location);
        }
        Path installedFile = file.toAbsolutePath().normalize();
        if (Files.isDirectory(installedFile)) {
            Path assetFile = installedFile.resolve(normalizedEntryName).normalize();
            if (!assetFile.startsWith(installedFile)) {
                throw new IOException("Theme-pack asset escapes the installed directory: " + normalizedEntryName);
            }
            if (!Files.isRegularFile(assetFile)) {
                throw new IOException("Installed theme-pack asset is missing: " + normalizedEntryName);
            }
            return new ThemePackResource.File(assetFile, normalizedEntryName);
        }
        if (!Files.isRegularFile(installedFile)) {
            throw new IOException("Installed theme-pack file is missing: " + installedFile);
        }

        try (ZipArchiveReader zipFile = new ZipArchiveReader(installedFile, StandardCharsets.UTF_8)) {
            ZipArchiveEntry entry = zipFile.getEntry(normalizedEntryName);
            if (entry == null || entry.isDirectory()) {
                throw new IOException("Installed theme-pack asset is missing: " + normalizedEntryName);
            }
        }
        return new ThemePackResource.Zip(installedFile, normalizedEntryName);
    }

View on GitHub (pinned to 24702dc5a0)