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

  1. Normalize the archive's entry names to remove '.', '..', and duplicate slashes, then repack.
  2. Reject or fix the source pack — this error can indicate a malicious archive, so do not bypass it.
  3. 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

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


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)