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
- Sanitize the user-supplied name: strip path separators and '..' segments, keep only the basename.
- Pass the correct parentPath that actually contains the intended workspace.
- Resolve the child against the parent and re-run checkDirectoryTraversal before any I/O.
- 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
- Always reduce user filenames to their basename before resolving.
- Reject absolute paths and any segment containing '..' from user input.
- Re-run checkDirectoryTraversal after resolving symlinks/normalizing.
- Never mirror traversal details back to the client.
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
- ESM UI provider output must expose a default PluginModule ex
- ESM UI provider output must contain at most one entry styles
- Unsupported URL protocol: {}
- The name from the request body does not match the plugin con
- Cookie token[1] did not contain a valid number (contained '{
AI-assisted analysis of halo-dev/halo@d2f5165f9c (2026-08-14).
Data as JSON: /api/errors/3bec12ef5f1aec24.
Report an issue: GitHub.