alibaba/nacos · critical · SecurityException

Path escapes target directory

Error message

Path escapes target directory: {target} is outside {baseDir}

What it means

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).

Solutions

  1. Ensure both baseDir and target are absolute and resolved (toRealPath) before comparing.
  2. Remove symlinks inside the base directory or resolve them and re-validate against the real base.
  3. Use Paths.get(baseDir).resolve(name).normalize() consistently so target is always derived from baseDir.

Example fix

// before
Path base = Paths.get("/skills");
Path target = Paths.get("/skills/../etc/hosts");
SkillUtils.validatePathContainment(base, target); // throws

// after
Path base = Paths.get("/skills").toAbsolutePath().normalize();
Path target = base.resolve("skillName/SKILL.md").normalize();
SkillUtils.validatePathContainment(base, target); // ok
Defensive patterns

Strategy: validation

Validate before calling

// Derive target from base so containment always holds
Path base = Paths.get(baseDir).toAbsolutePath().normalize();
Path target = base.resolve(relativeName).normalize();
SkillUtils.validatePathContainment(base, target);

Type guard

static boolean isContained(Path base, Path target) {
    Path b = base.toAbsolutePath().normalize();
    Path t = target.toAbsolutePath().normalize();
    return t.startsWith(b);
}

Try / catch

try {
    SkillUtils.validatePathContainment(baseDir, target);
} catch (SecurityException e) {
    // containment violation — do not write the file
    throw new IOException("Refusing to write outside base dir", e);
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of alibaba/nacos@9b989acdf1 (2026-08-14). Data as JSON: /api/errors/2a913403f4890310. Report an issue: GitHub.

Appendix: source

Thrown at api/src/main/java/com/alibaba/nacos/api/ai/model/skills/SkillUtils.java:189

        }
        if (path.contains(PATH_TRAVERSAL_SEQUENCE)) {
            throw new SecurityException("Path traversal detected: " + path);
        }
        if (path.startsWith("/") || path.startsWith("\\")) {
            throw new SecurityException("Absolute path not allowed: " + path);
        }
    }
    
    /**
     * Validate that a resolved path stays within the expected base directory.
     *
     * @param baseDir the base directory that must contain the target
     * @param target  the resolved target path
     * @throws SecurityException if target escapes baseDir
     */
    public static void validatePathContainment(Path baseDir, Path target) {
        if (!target.normalize().startsWith(baseDir.normalize())) {
            throw new SecurityException(
                "Path escapes target directory: " + target + " is outside " + baseDir);
        }
    }
    
    /**
     * Validate that byte array is a valid ZIP file by checking the magic number header.
     *
     * @param data the byte array to validate
     * @throws IllegalArgumentException if data is null, too short, or does not have ZIP magic header
     */
    public static void validateZipBytes(byte[] data) {
        if (data == null || data.length < ZIP_MIN_SIZE) {
            throw new IllegalArgumentException(
                "Invalid ZIP data: too short (" + (data == null ? 0 : data.length) + " bytes)");
        }
        for (int i = 0; i < ZIP_MAGIC.length; i++) {
            if (data[i] != ZIP_MAGIC[i]) {
                throw new IllegalArgumentException(

View on GitHub (pinned to 9b989acdf1)