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
- Sanitize the entry name before calling: strip '..' segments and leading separators, e.g. via ThemePackAsset.normalizeEntryName on your own inputs.
- Only resolve entry names that come from the pack's own manifest, never from untrusted input.
- 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
- Never feed untrusted strings (config, network, user input) as entry names
- Only use entry names read from the pack's own manifest
- Run normalizeEntryName and reject '..' segments at your API boundary
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
- Theme-pack asset entry contains an unsafe segment:
- Malformed modpack configuration
- Theme pack location has no readable file:
- Installed theme-pack asset is missing:
- Installed theme-pack file is missing:
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)