{"record":{"id":"7a5d0668103dcc43","repo":"conductor-oss/conductor","slug":"access-denied-path-is-not-under-any-allowed-direc","errorCode":null,"errorMessage":"Access denied: path is not under any allowed directory. Allowed directories: {dirs}","messagePattern":"Access denied: path is not under any allowed directory\\. Allowed directories: (.+?)","errorType":"validation","errorClass":"DocumentAccessDeniedException","httpStatus":null,"severity":"warning","filePath":"ai/src/main/java/org/conductoross/conductor/ai/document/DocumentAccessPolicy.java","lineNumber":411,"sourceCode":"     */\n    private void checkAllowedDirectories(String originalLocation, String normalizedPath) {\n        List<String> dirs = effectiveAllowedDirectories;\n        if (dirs == null || dirs.isEmpty()) {\n            return;\n        }\n        // Only apply to local filesystem paths, not HTTP URLs\n        if (originalLocation.startsWith(\"http://\") || originalLocation.startsWith(\"https://\")) {\n            return;\n        }\n\n        for (String dir : dirs) {\n            String expandedDir = expandHome(dir.endsWith(\"/\") ? dir : dir + \"/\");\n            if (normalizedPath.startsWith(expandedDir) || normalizedPath.equals(expandedDir)) {\n                return; // Path is within an allowed directory\n            }\n        }\n\n        throw new DocumentAccessDeniedException(\n                \"Access denied: path is not under any allowed directory. \"\n                        + \"Allowed directories: \"\n                        + dirs);\n    }\n\n    private String normalizeLocation(String location) {\n        // Strip file:// scheme\n        String path = location;\n        if (path.startsWith(\"file://\")) {\n            path = path.substring(7);\n        }\n\n        // For HTTP URLs, extract the path component\n        if (path.startsWith(\"http://\") || path.startsWith(\"https://\")) {\n            try {\n                URI uri = URI.create(path);\n                return uri.getPath() != null ? uri.getPath() : \"\";\n            } catch (Exception e) {","sourceCodeStart":393,"sourceCodeEnd":429,"githubUrl":"https://github.com/conductor-oss/conductor/blob/cf7c3e4a8adfb158be778ab1ec525323c363cd3a/ai/src/main/java/org/conductoross/conductor/ai/document/DocumentAccessPolicy.java#L393-L429","documentation":"Thrown by DocumentAccessPolicy.checkAllowedDirectories when a local (non-http) path does not start with any effective allowed directory. The effective list is computed at startup: conductor.file-storage.parentDir (defaulting to ~/worker-payload/) plus conductor.document-access-policy.allowed-directories. Only when the list is non-empty does this allowlist enforce — meaning by default, local files must live under the file-storage tree. DocumentAccessDeniedException (SecurityException); the message lists the allowed directories.","triggerScenarios":"A document loader/upload is given a local file path that lives outside ~/worker-payload/ and outside any extra allowed-directories you configured.","commonSituations":"A workflow reads a file from /tmp or an arbitrary absolute path that is not under the storage root; parentDir was changed and old file references now fall outside; a new import location was not added to allowed-directories.","solutions":["Move/copy the file under conductor.file-storage.parentDir (or an allowed-directories entry).","Add the directory to conductor.document-access-policy.allowed-directories (supports ~ expansion), e.g. /data/imports/.","Confirm the path you pass is absolute and normalized so the startsWith check matches (trailing slashes matter)."],"exampleFix":"# application.yml — before (default only ~/worker-payload/)\n# after — add an import location\nconductor:\n  document-access-policy:\n    allowed-directories:\n      - /data/imports/","handlingStrategy":"validation","validationCode":"// Confirm a local path is under an allowed dir before calling the loader\njava.util.List<String> allowed = accessPolicy.getEffectiveAllowedDirectories();\nString norm = java.nio.file.Path.of(path).normalize().toString();\nboolean ok = allowed.stream().anyMatch(d -> {\n    String base = d.endsWith(\"/\") ? d : d + \"/\";\n    return norm.startsWith(base) || norm.equals(base);\n});\nif (!ok) throw new IllegalArgumentException(\"Path not under an allowed dir: \" + path);","typeGuard":null,"tryCatchPattern":"try {\n    loader.download(path);\n} catch (SecurityException e) {\n    // add the directory to allowed-directories or move the file under parentDir\n    log.warn(\"Path outside allowed dirs {}; allowed={}\", path, accessPolicy.getEffectiveAllowedDirectories());\n    throw e;\n}","preventionTips":["Keep documents under conductor.file-storage.parentDir by default.","Add extra roots to conductor.document-access-policy.allowed-directories explicitly.","Pass absolute, normalized paths so the startsWith allowlist check matches."],"tags":["security","access-control","document-loader","config","allowlist"],"backgroundTag":null,"analyzedSha":"cf7c3e4a8adfb158be778ab1ec525323c363cd3a","analyzedAt":"2026-08-14T03:33:19.897Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}