{"record":{"id":"729782da75dd8bd8","repo":"Mintplex-Labs/anything-llm","slug":"access-denied-symlink-target-outside-allowed-dir","errorCode":null,"errorMessage":"Access denied - symlink target outside allowed directories.","messagePattern":"Access denied - symlink target outside allowed directories\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/utils/agents/aibitat/plugins/filesystem/lib.js","lineNumber":444,"sourceCode":"      console.log(\n        `[validatePath] Access denied - path outside allowed directories: ${absolute} not in ${this.#allowedDirectories.join(\", \")}`\n      );\n      throw new Error(`Access denied - path outside allowed directories.`);\n    }\n\n    try {\n      const realPath = await fs.realpath(absolute);\n      const normalizedReal = this.#normalizePath(realPath);\n      if (\n        !this.#isPathWithinAllowedDirectories(\n          normalizedReal,\n          this.#allowedDirectories\n        )\n      ) {\n        console.log(\n          `[validatePath] Access denied - symlink target outside allowed directories: ${realPath} not in ${this.#allowedDirectories.join(\", \")}`\n        );\n        throw new Error(\n          `Access denied - symlink target outside allowed directories.`\n        );\n      }\n      return realPath;\n    } catch (error) {\n      if (error.code === \"ENOENT\") {\n        const parentDir = path.dirname(absolute);\n        try {\n          const realParentPath = await fs.realpath(parentDir);\n          const normalizedParent = this.#normalizePath(realParentPath);\n          if (\n            !this.#isPathWithinAllowedDirectories(\n              normalizedParent,\n              this.#allowedDirectories\n            )\n          ) {\n            console.log(\n              `[validatePath] Access denied - parent directory outside allowed directories: ${realParentPath} not in ${this.#allowedDirectories.join(\", \")}`","sourceCodeStart":426,"sourceCodeEnd":462,"githubUrl":"https://github.com/Mintplex-Labs/anything-llm/blob/3aec848f2885144aa8f1e53b9731a04310d5d558/server/utils/agents/aibitat/plugins/filesystem/lib.js#L426-L462","documentation":"Thrown by validatePath when fs.realpath() resolves the requested path (via symlink) to a physical location outside the allowed directories. Even though the requested path string sits inside the sandbox, the operating system would actually read/write the link target, so the library rejects it. This blocks symlink-escape attacks against the agent filesystem sandbox.","triggerScenarios":"A file inside <storage>/anythingllm-fs is a symlink to /etc, /root, or any directory outside the allowed roots; agent writes to a symlinked path whose target is on another mount; a previously-clean path becomes a symlink after a package or setup step creates links in the workspace.","commonSituations":"User symlinks a folder into the sandbox to 'share' documents with the agent; ln -s /var/data ./storage/anythingllm-fs/data in a Docker volume setup; CI copies a tree that contains absolute symlinks that only resolve on the build host.","solutions":["Remove or replace the symlink with a real copy of the target inside the allowed directory.","If the target location must be accessible, initialize the filesystem library with the real target directory added to the allowed list.","Inspect with ls -la / readlink -f <path> to confirm which link escapes, then fix that specific link."],"exampleFix":"# before\nln -s /var/reports /app/storage/anythingllm-fs/reports\nread_file({ path: \"reports/q3.txt\" })  # throws: symlink target outside\n\n# after\ncp -r /var/reports /app/storage/anythingllm-fs/reports\nread_file({ path: \"reports/q3.txt\" })  # ok","handlingStrategy":"try-catch","validationCode":"const fs = require(\"fs\").promises;\nconst path = require(\"path\");\nasync function assertNoSymlinkEscape(fileOps, target) {\n  const allowed = fileOps.getAllowedDirectories();\n  const real = await fs.realpath(target).catch(() => null);\n  if (real && !allowed.some((r) => real === r || real.startsWith(r + path.sep))) {\n    throw new Error(`Refusing ${target}: symlink resolves to ${real}, outside sandbox`);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  const validated = await fileOps.validatePath(p);\n} catch (e) {\n  if (e.message.includes(\"symlink target outside allowed directories\")) {\n    // security event: log path + realpath, reject; do not auto-retry or rewrite the path\n    securityLog(`symlink escape attempt: ${p}`);\n    return;\n  }\n  throw e;\n}","preventionTips":["Audit the sandbox root for symlinks before granting the agent access: find <root> -type l -exec readlink -f {} \\;","Replace symlinks with copies or bind-mounts that resolve inside the sandbox.","Never auto-add a symlink's target to allowed directories in response to this error without human review."],"tags":["filesystem","symlink","security","sandbox","realpath"],"backgroundTag":"symlink-escape-blocked","analyzedSha":"3aec848f2885144aa8f1e53b9731a04310d5d558","analyzedAt":"2026-08-18T10:02:21.017Z","contentChangedAt":"2026-08-18T10:02:21.017Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}