halo-dev/halo · critical · AccessDeniedException

problemDetail.directoryTraversal

problemDetail.directoryTraversal

Error message

Directory traversal detected: {pathToCheck}

What it means

Thrown as Halo's run.halo.app.infra.exception.AccessDeniedException (carrying problemDetail code 'problemDetail.directoryTraversal') by FileUtils.checkDirectoryTraversal when pathToCheck.normalize() does not start with parentPath. This is a security guard against path-traversal in user-supplied filenames/paths so writes stay inside the allowed parent.

Source

Thrown at application/src/main/java/run/halo/app/infra/utils/FileUtils.java:232

            }
        }
    }

    /**
     * Checks directory traversal vulnerability.
     *
     * @param parentPath parent path must not be null.
     * @param pathToCheck path to check must not be null
     */
    public static void checkDirectoryTraversal(Path parentPath, Path pathToCheck) {
        Assert.notNull(parentPath, "Parent path must not be null");
        Assert.notNull(pathToCheck, "Path to check must not be null");

        if (pathToCheck.normalize().startsWith(parentPath)) {
            return;
        }

        throw new AccessDeniedException(
                "Directory traversal detected: " + pathToCheck,
                "problemDetail.directoryTraversal",
                new Object[] {parentPath, pathToCheck});
    }

    /**
     * Checks directory traversal vulnerability.
     *
     * @param parentPath parent path must not be null.
     * @param pathToCheck path to check must not be null
     */
    public static void checkDirectoryTraversal(String parentPath, String pathToCheck) {
        checkDirectoryTraversal(Paths.get(parentPath), Paths.get(pathToCheck));
    }

    /**
     * Checks directory traversal vulnerability.
     *

View on GitHub (pinned to d2f5165f9c)

Solutions

  1. Sanitize the user-supplied name: strip path separators and '..' segments, keep only the basename.
  2. Pass the correct parentPath that actually contains the intended workspace.
  3. Resolve the child against the parent and re-run checkDirectoryTraversal before any I/O.
  4. Reject absolute paths in user input outright.

Example fix

// before
var target = Paths.get(fileName); // fileName = ../etc/passwd
FileUtils.checkDirectoryTraversal(parentDir, target);

// after
var safe = parentDir.resolve(Path.of(fileName).getFileName().toString());
FileUtils.checkDirectoryTraversal(parentDir, safe);
Defensive patterns

Strategy: validation

Validate before calling

// Reject user paths that escape parent BEFORE any I/O
String base = Path.of(userPath).getFileName().toString(); // drop all dir info
Path safe = parentPath.resolve(base).normalize();
FileUtils.checkDirectoryTraversal(parentPath, safe);

Type guard

static boolean isInsideParent(Path parent, Path child) {
    return child.normalize().startsWith(parent.normalize());
}

Try / catch

try {
    FileUtils.checkDirectoryTraversal(parentPath, pathToCheck);
} catch (run.halo.app.infra.exception.AccessDeniedException e) {
    // security control: do NOT echo parentPath to the client; return generic 403
    return ServerResponse.status(HttpStatus.FORBIDDEN).build();
}

Prevention

When it happens

Trigger: A user/plugin-supplied relative path containing '..' or an absolute path that, once normalized, escapes the configured parent directory; e.g. uploading/reading a file whose name is '../../etc/passwd'.

Common situations: Attachment/upload handlers receiving malicious filenames; theme/plugin asset paths built from user input; symbolic links resolving outside the parent; incorrect parentPath base passed by a misconfigured caller.

Related errors


AI-assisted analysis of halo-dev/halo@d2f5165f9c (2026-08-14). Data as JSON: /api/errors/3bec12ef5f1aec24. Report an issue: GitHub.