{"record":{"id":"8b20cd3f762f5635","repo":"HMCL-dev/HMCL","slug":"theme-pack-asset-escapes-the-installed-directory","errorCode":null,"errorMessage":"Theme-pack asset escapes the installed directory: ","messagePattern":"Theme-pack asset escapes the installed directory: ","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"HMCL/src/main/java/org/jackhuang/hmcl/theme/ThemePackManager.java","lineNumber":1212,"sourceCode":"    /// @param entryName the theme-pack relative asset entry name\n    /// @return the resolved resource\n    /// @throws IOException if the asset cannot be read\n    public static ThemePackResource resolveInstalledAsset(ThemePackLocation location, String entryName) throws IOException {\n        Objects.requireNonNull(location);\n        String normalizedEntryName = ThemePackAsset.normalizeEntryName(entryName);\n        if (location instanceof ThemePackLocation.Builtin builtin) {\n            return resolveBuiltinAsset(builtin, normalizedEntryName);\n        }\n\n        @Nullable Path file = location.file();\n        if (file == null) {\n            throw new IOException(\"Theme pack location has no readable file: \" + location);\n        }\n        Path installedFile = file.toAbsolutePath().normalize();\n        if (Files.isDirectory(installedFile)) {\n            Path assetFile = installedFile.resolve(normalizedEntryName).normalize();\n            if (!assetFile.startsWith(installedFile)) {\n                throw new IOException(\"Theme-pack asset escapes the installed directory: \" + normalizedEntryName);\n            }\n            if (!Files.isRegularFile(assetFile)) {\n                throw new IOException(\"Installed theme-pack asset is missing: \" + normalizedEntryName);\n            }\n            return new ThemePackResource.File(assetFile, normalizedEntryName);\n        }\n        if (!Files.isRegularFile(installedFile)) {\n            throw new IOException(\"Installed theme-pack file is missing: \" + installedFile);\n        }\n\n        try (ZipArchiveReader zipFile = new ZipArchiveReader(installedFile, StandardCharsets.UTF_8)) {\n            ZipArchiveEntry entry = zipFile.getEntry(normalizedEntryName);\n            if (entry == null || entry.isDirectory()) {\n                throw new IOException(\"Installed theme-pack asset is missing: \" + normalizedEntryName);\n            }\n        }\n        return new ThemePackResource.Zip(installedFile, normalizedEntryName);\n    }","sourceCodeStart":1194,"sourceCodeEnd":1230,"githubUrl":"https://github.com/HMCL-dev/HMCL/blob/24702dc5a0214034f4c27166d5fd30cad08cec19/HMCL/src/main/java/org/jackhuang/hmcl/theme/ThemePackManager.java#L1194-L1230","documentation":"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.","triggerScenarios":"Calling resolveInstalledAsset() with an entryName like \"../../etc/passwd\" or an absolute/symlink-escaping name whose normalized resolution does not start with the installed directory.","commonSituations":"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.","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."],"exampleFix":"// before\nresolveInstalledAsset(location, untrustedName);\n// after\nString safe = ThemePackAsset.normalizeEntryName(untrustedName);\nif (!safe.contains(\"..\")) resolveInstalledAsset(location, safe);","handlingStrategy":"validation","validationCode":"String normalized = ThemePackAsset.normalizeEntryName(entryName);\nif (normalized.contains(\"..\") || normalized.startsWith(\"/\")) {\n    throw new IllegalArgumentException(\"unsafe asset entry: \" + entryName);\n}","typeGuard":"static boolean isSafeEntryName(String entry) {\n    String n = ThemePackAsset.normalizeEntryName(entry);\n    return !n.isEmpty() && !n.contains(\"..\") && !n.startsWith(\"/\") && !n.contains(\"\\\\\");\n}","tryCatchPattern":"try {\n    ThemePackManager.resolveInstalledAsset(location, entry);\n} catch (IOException e) {\n    if (e.getMessage().startsWith(\"Theme-pack asset escapes the installed directory\")) {\n    // reject the pack; treat as untrusted input\n    }\n}","preventionTips":["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"],"tags":["theme-pack","assets","path-traversal","security"],"backgroundTag":"path-traversal-blocked","analyzedSha":"24702dc5a0214034f4c27166d5fd30cad08cec19","analyzedAt":"2026-09-10T12:36:46.680Z","contentChangedAt":"2026-09-10T12:36:46.680Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}