{"record":{"id":"b8ca99fc4ce30928","repo":"HMCL-dev/HMCL","slug":"description-path-escapes-instance-root-targe","errorCode":null,"errorMessage":"${description} path escapes instance root: ${target}","messagePattern":"(.+?) path escapes instance root: (.+?)","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositoryDraft.java","lineNumber":466,"sourceCode":"        validateInstanceFileTarget(id, target, \"Primary JAR\");\n        return target;\n    }\n\n    /// Verifies that a file target is a strict descendant of its instance root.\n    ///\n    /// @param id          the owning instance id\n    /// @param target      the normalized target path\n    /// @param description description used in an exception message\n    /// @throws IOException if the target is outside the instance root\n    private void validateInstanceFileTarget(\n            GameInstanceID id,\n            Path target,\n            String description) throws IOException {\n        Path expectedRoot = baseSnapshot.getLayout().getInstanceRoot(id)\n                .toAbsolutePath()\n                .normalize();\n        if (target.equals(expectedRoot) || !target.startsWith(expectedRoot)) {\n            throw new IOException(description + \" path escapes instance root: \" + target);\n        }\n    }\n\n    /// Creates a directory for rollback data produced by the current commit attempt.\n    ///\n    /// @return the new rollback directory\n    /// @throws IOException if the directory cannot be created\n    private Path createRollbackDirectory() throws IOException {\n        Path parent = baseSnapshot.getLayout().getBaseDirectory()\n                .toAbsolutePath()\n                .normalize()\n                .resolve(\".hmcl\")\n                .resolve(\"repository-drafts\");\n        Files.createDirectories(parent);\n        return Files.createTempDirectory(parent, \"commit-\");\n    }\n\n    /// Applies one instance directory rename.","sourceCodeStart":448,"sourceCodeEnd":484,"githubUrl":"https://github.com/HMCL-dev/HMCL/blob/24702dc5a0214034f4c27166d5fd30cad08cec19/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositoryDraft.java#L448-L484","documentation":"DefaultGameRepositoryDraft.validateInstanceFileTarget rejects any candidate file target that is not strictly inside the instance's root directory. The library throws this to prevent instance file operations (jar replacement, manifest application) from writing or reading outside the per-instance directory, i.e. a path-traversal containment check. It fires whenever the target resolves to the instance root itself or lies outside it.","triggerScenarios":"Calling getPrimaryJarTarget or applyManifest when the constructed target Path is absolute-unnormalized, contains '..' segments, or symlinks to a location outside getInstanceRoot(id); also when target equals the instance root itself.","commonSituations":"Crafted or corrupted instance manifests referencing ../ paths; symlinked instance directories; manually edited version JSON with an absolute jar path; tests passing fabricated Paths.","solutions":["Normalize and resolve the target path (toAbsolutePath().normalize(), resolve against instance root) before passing it to the API","Ensure the target is a relative path composed from getInstanceRoot(id), not a user- or manifest-supplied absolute path","Check for symlinks (Files.isSymbolicLink on parents) and use toRealPath() when containment matters","Inspect the instance manifest for suspicious jar/inheritsFrom path values and correct them"],"exampleFix":"// before\nPath target = Paths.get(manifest.jarPath());\nrepo.applyManifest(id, manifest, target);\n// after\nPath target = repo.getInstanceRoot(id).resolve(manifest.jarPath()).normalize();\nrepo.applyManifest(id, manifest, target);","handlingStrategy":"validation","validationCode":"Path root = repo.getInstanceRoot(id).toAbsolutePath().normalize();\nPath target = candidate.toAbsolutePath().normalize();\nif (target.equals(root) || !target.startsWith(root)) throw new IllegalArgumentException(\"target escapes instance root\");","typeGuard":"static boolean isInside(Path root, Path candidate) {\n    Path r = root.toAbsolutePath().normalize();\n    Path t = candidate.toAbsolutePath().normalize();\n    return !t.equals(r) && t.startsWith(r);\n}","tryCatchPattern":"try { repo.applyManifest(id, manifest, target); }\ncatch (IOException e) { if (e.getMessage().contains(\"escapes instance root\")) { LOG.warning(\"Blocked path traversal: \" + target); } else throw e; }","preventionTips":["Always build targets with instanceRoot.resolve(relative).normalize()","Never pass raw manifest/user-supplied paths as absolute targets","Resolve symlinks with toRealPath() before containment checks"],"tags":["io","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"}