HMCL-dev/HMCL · error · IOException
Theme-pack entry contains an unsafe segment
Error message
Theme-pack entry contains an unsafe segment: ${entryName} What it means
Each slash-separated segment of a normalized theme-pack entry name must be a real path segment. Empty segments, '.', or '..' make the path ambiguous or escaping, so ThemePackManager throws this IOException. This prevents zip-slip attacks where '..' entries would extract outside the pack directory.
Solutions
- Normalize the archive's entry names to remove '.', '..', and duplicate slashes, then repack.
- Reject or fix the source pack — this error can indicate a malicious archive, so do not bypass it.
- Use ThemePackExporter, which writes canonical entry names.
Example fix
// before
String name = "assets/" + userSubdir + "/bg.png"; // userSubdir may be ".."
// after
String name = packRoot.relativize(packRoot.resolve("assets", userSubdir, "bg.png").normalize());
if (name.startsWith("..") ) throw new IllegalArgumentException("entry escapes pack root"); Defensive patterns
Strategy: validation
Validate before calling
for (var e : Collections.list(new ZipFile(pack).entries())) {
for (String seg : e.getName().replace('\\', '/').split("/")) {
if (seg.isEmpty() || seg.equals(".") || seg.equals("..")) throw new IllegalArgumentException("unsafe segment in " + e.getName());
}
} Try / catch
try {
ThemePackManager.install(pack, dir);
} catch (IOException e) {
if (e.getMessage().contains("unsafe segment")) {
LOG.warn("Rejected theme pack with traversal segments — treat as untrusted");
ui.show("Theme pack rejected: unsafe entry paths.");
} else throw e;
} Prevention
- Treat '..'-containing packs as potentially malicious — never bypass the check.
- Normalize paths (Path.normalize + relativize) before writing entries.
- Only install packs from trusted sources.
When it happens
Trigger: Installing a zip containing entries like 'assets/../theme-pack.json', 'assets//bg.png', or './theme-pack.json'.
Common situations: Archives built by scripts that concatenate path components without normalizing; malicious or careless repacking introducing '..' traversal segments; tools emitting double slashes.
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
- Malformed modpack configuration
- Theme-pack asset entry contains an unsafe segment:
- Theme-pack asset escapes the installed directory:
- Theme-pack entry contains a control character
- path escapes instance root
AI-assisted analysis of HMCL-dev/HMCL@24702dc5a0 (2026-09-10).
Data as JSON: /api/errors/4f3e7f813446e5e8.
Report an issue: GitHub.
Appendix: source
Thrown at HMCL/src/main/java/org/jackhuang/hmcl/theme/ThemePackManager.java:1411
/// Returns a normalized and safe theme-pack zip entry name.
private static String normalizeThemePackEntryName(String entryName) throws IOException {
Objects.requireNonNull(entryName);
String normalized = entryName.trim().replace('\\', '/');
if (normalized.endsWith("/")) {
normalized = normalized.substring(0, normalized.length() - 1);
}
if (normalized.isEmpty()) {
throw new IOException("Theme-pack entry is empty");
}
if (normalized.startsWith("/") || normalized.matches("^[A-Za-z]:.*")) {
throw new IOException("Theme-pack entry must be relative: " + entryName);
}
for (String segment : normalized.split("/")) {
if (segment.isEmpty() || ".".equals(segment) || "..".equals(segment)) {
throw new IOException("Theme-pack entry contains an unsafe segment: " + entryName);
}
for (int i = 0; i < segment.length(); i++) {
char ch = segment.charAt(i);
if (Character.isISOControl(ch) || ch == '\0') {
throw new IOException("Theme-pack entry contains a control character: " + entryName);
}
}
}
return normalized;
}
/// Checks that a theme-pack zip entry belongs to the current file layout.
private static void checkSupportedThemePackEntry(String entryName) throws IOException {
if (!ThemePackExporter.MANIFEST_ENTRY.equals(entryName)
&& !"assets".equals(entryName)
&& !entryName.startsWith("assets/")) {
throw new IOException("Unsupported theme-pack entry: " + entryName);
}View on GitHub (pinned to 24702dc5a0)