{"record":{"id":"2a913403f4890310","repo":"alibaba/nacos","slug":"path-escapes-target-directory-target-is-outside","errorCode":null,"errorMessage":"Path escapes target directory: {target} is outside {baseDir}","messagePattern":"Path escapes target directory: (.+?) is outside (.+?)","errorType":"exception","errorClass":"SecurityException","httpStatus":null,"severity":"critical","filePath":"api/src/main/java/com/alibaba/nacos/api/ai/model/skills/SkillUtils.java","lineNumber":189,"sourceCode":"        }\n        if (path.contains(PATH_TRAVERSAL_SEQUENCE)) {\n            throw new SecurityException(\"Path traversal detected: \" + path);\n        }\n        if (path.startsWith(\"/\") || path.startsWith(\"\\\\\")) {\n            throw new SecurityException(\"Absolute path not allowed: \" + path);\n        }\n    }\n    \n    /**\n     * Validate that a resolved path stays within the expected base directory.\n     *\n     * @param baseDir the base directory that must contain the target\n     * @param target  the resolved target path\n     * @throws SecurityException if target escapes baseDir\n     */\n    public static void validatePathContainment(Path baseDir, Path target) {\n        if (!target.normalize().startsWith(baseDir.normalize())) {\n            throw new SecurityException(\n                \"Path escapes target directory: \" + target + \" is outside \" + baseDir);\n        }\n    }\n    \n    /**\n     * Validate that byte array is a valid ZIP file by checking the magic number header.\n     *\n     * @param data the byte array to validate\n     * @throws IllegalArgumentException if data is null, too short, or does not have ZIP magic header\n     */\n    public static void validateZipBytes(byte[] data) {\n        if (data == null || data.length < ZIP_MIN_SIZE) {\n            throw new IllegalArgumentException(\n                \"Invalid ZIP data: too short (\" + (data == null ? 0 : data.length) + \" bytes)\");\n        }\n        for (int i = 0; i < ZIP_MAGIC.length; i++) {\n            if (data[i] != ZIP_MAGIC[i]) {\n                throw new IllegalArgumentException(","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/alibaba/nacos/blob/9b989acdf181d00898f2e8839257bb2b2a3cefe3/api/src/main/java/com/alibaba/nacos/api/ai/model/skills/SkillUtils.java#L171-L207","documentation":"Thrown by SkillUtils.validatePathContainment as a SecurityException when a resolved target Path, after normalization, does not start with the normalized base directory. This is the containment backstop that catches path-escape attempts the lexical '..'/absolute checks might miss (e.g. symlink resolution or platform-specific normalization).","triggerScenarios":"Calling validatePathContainment(baseDir, target) where target.normalize().startsWith(baseDir.normalize()) is false — for example base=/skills, target=/skills/../etc/hosts (resolved), or a symlink in the skill tree resolves outside base.","commonSituations":"A base directory containing a symlink that points outside the tree; a resolved resource path that escapes after normalization due to '..' segments surviving an earlier weak check; mismatched absolute vs relative base and target paths.","solutions":["Ensure both baseDir and target are absolute and resolved (toRealPath) before comparing.","Remove symlinks inside the base directory or resolve them and re-validate against the real base.","Use Paths.get(baseDir).resolve(name).normalize() consistently so target is always derived from baseDir."],"exampleFix":"// before\nPath base = Paths.get(\"/skills\");\nPath target = Paths.get(\"/skills/../etc/hosts\");\nSkillUtils.validatePathContainment(base, target); // throws\n\n// after\nPath base = Paths.get(\"/skills\").toAbsolutePath().normalize();\nPath target = base.resolve(\"skillName/SKILL.md\").normalize();\nSkillUtils.validatePathContainment(base, target); // ok","handlingStrategy":"validation","validationCode":"// Derive target from base so containment always holds\nPath base = Paths.get(baseDir).toAbsolutePath().normalize();\nPath target = base.resolve(relativeName).normalize();\nSkillUtils.validatePathContainment(base, target);","typeGuard":"static boolean isContained(Path base, Path target) {\n    Path b = base.toAbsolutePath().normalize();\n    Path t = target.toAbsolutePath().normalize();\n    return t.startsWith(b);\n}","tryCatchPattern":"try {\n    SkillUtils.validatePathContainment(baseDir, target);\n} catch (SecurityException e) {\n    // containment violation — do not write the file\n    throw new IOException(\"Refusing to write outside base dir\", e);\n}","preventionTips":["Always derive target paths via base.resolve(name).normalize() rather than accepting caller-supplied absolute paths.","Resolve symlinks (toRealPath) under the base before containment checks.","Treat a containment failure as a security incident, not a recoverable error."],"tags":["java","nacos","ai","skills","security","path-traversal","filesystem"],"backgroundTag":null,"analyzedSha":"9b989acdf181d00898f2e8839257bb2b2a3cefe3","analyzedAt":"2026-08-14T07:17:31.569Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}