{"record":{"id":"6f3944d94b32fdb9","repo":"appsmithorg/appsmith","slug":"pe-plg-5000","errorCode":"PE-PLG-5000","errorMessage":"SECURITY: Unable to resolve real path for {} while validating Git root containment","messagePattern":"SECURITY: Unable to resolve real path for (.+?) while validating Git root containment","errorType":"error_code","errorClass":"AppsmithPluginException","httpStatus":500,"severity":"critical","filePath":"app/server/appsmith-git/src/main/java/com/appsmith/git/files/FileUtilsCEImpl.java","lineNumber":374,"sourceCode":"        if (!normalizedTarget.startsWith(gitRoot)) {\n            throwPathTraversal(normalizedTarget, gitRoot);\n        }\n\n        // 2. Symlink-aware containment check (GHSA-fqwc-g9wm-5895) — blocks symbolic links committed\n        // inside a repository that point outside the Git root. Path.normalize() above is purely\n        // lexical and does NOT resolve symlinks, whereas every downstream file I/O sink follows\n        // them. Resolve the real (symlink-free) path before comparing. Fails closed on I/O error.\n        try {\n            Path realGitRoot = toRealPathResolvingExistingPrefix(gitRoot);\n            Path realTarget = toRealPathResolvingExistingPrefix(normalizedTarget);\n            if (!realTarget.startsWith(realGitRoot)) {\n                throwPathTraversal(realTarget, realGitRoot);\n            }\n        } catch (IOException e) {\n            String errorMessage = \"SECURITY: Unable to resolve real path for \" + normalizedTarget\n                    + \" while validating Git root containment\";\n            log.error(errorMessage, e);\n            throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, errorMessage);\n        }\n    }\n\n    private void throwPathTraversal(Path attemptedPath, Path gitRoot) {\n        String errorMessage = \"SECURITY: Path traversal detected. Attempted to access \" + attemptedPath\n                + \" which is outside the Git root \" + gitRoot;\n        log.error(errorMessage);\n        throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, errorMessage);\n    }\n\n    /**\n     * Resolves symbolic links in the longest existing prefix of {@code path} and re-appends the\n     * remaining (not-yet-created) path segments lexically. {@link Path#toRealPath} cannot be used\n     * directly because it requires the whole path to exist, while file writes legitimately target\n     * paths that do not exist yet. By resolving the deepest existing ancestor we still detect any\n     * symlink along the existing portion (including when {@code path} itself is a symlink) while\n     * supporting yet-to-be-created files.\n     */","sourceCodeStart":356,"sourceCodeEnd":392,"githubUrl":"https://github.com/appsmithorg/appsmith/blob/8cd9021c24cdbea1c3c12c966073708e83db60c2/app/server/appsmith-git/src/main/java/com/appsmith/git/files/FileUtilsCEImpl.java#L356-L392","documentation":"Thrown by FileUtilsCEImpl.validatePathIsWithinGitRoot when resolving symbolic links for the symlink-aware containment check (the defense for GHSA-fqwc-g9wm-5895). The code intentionally fails closed: if toRealPathResolvingExistingPrefix(gitRoot) or toRealPathResolvingExistingPrefix(target) raises an IOException (permissions denied, dangling symlink, I/O error on the deepest existing ancestor), it logs the error and throws AppsmithPluginException(PLUGIN_ERROR, code PE-PLG-5000) rather than letting the operation proceed with an unvalidated path.","triggerScenarios":"A git operation (commit, pull, connect, import, file read/write) on a path whose existing ancestor cannot be resolved: the git root or a target directory is on a filesystem that returns I/O errors on toRealPath, a symlink in the path is dangling, or the process lacks read/execute permission on an ancestor directory.","commonSituations":"Misconfigured git root path (gitServiceConfig.getGitRootPath) pointing at an unreadable/nonexistent location; container volume-mount permission issues; a symlink left inside the repo from a prior clone that now dangles; restrictive filesystem ACLs on the data root.","solutions":["Verify the configured git root path exists and is readable/executable by the Appsmith server user: ls -la <gitRoot>.","Find and remove dangling symlinks inside the repo root: find <gitRoot> -xtype l.","Check filesystem/container permissions for the volume holding the git root; remount with correct uid/gid.","Re-run the git operation after fixing permissions; the check fails closed so it will pass once resolution succeeds.","Inspect the server log: the IOException is logged with the SECURITY message and shows the exact path that failed to resolve."],"exampleFix":"// before\n# git root unreadable by appsmith user\nsudo chown -R appsmith:appsmith /data/gitroot\n\n// after\n# readable + resolvable; containment check now succeeds\nls -la /data/gitroot  # confirms ownership and no dangling links","handlingStrategy":"validation","validationCode":"# Pre-flight: confirm the git root and ancestors are readable/resolvable.\ngit_root=\"$(config_get_gitRootPath)\"\n[ -d \"$git_root\" ] || echo \"git root missing\"\nfind \"$git_root\" -xtype l -print        # list dangling symlinks\nstat -c '%U:%G %a %n' \"$git_root\"       # check ownership/perm","typeGuard":"// Java caller-side guard before invoking FileUtils\nPath root = Paths.get(config.getGitRootPath()).toAbsolutePath().normalize();\nif (!Files.isDirectory(root) || !Files.isReadable(root)) {\n  throw new IllegalStateException(\"Git root not readable: \" + root);\n}","tryCatchPattern":"try {\n  fileUtils.validatePathIsWithinGitRoot(target);\n} catch (AppsmithPluginException e) {\n  if (e.getCode() == AppsmithPluginError.PLUGIN_ERROR) {\n    log.error(\"Git root containment check failed (I/O); verify permissions/symlinks\", e);\n  }\n  throw e;\n}","preventionTips":["Ensure the configured git root exists and is readable/executable by the Appsmith server user.","Periodically remove dangling symlinks: find <gitRoot> -xtype l -delete.","Mount container volumes with the correct uid/gid for the server user.","Never widen or bypass the check to 'make it work'; fix the underlying I/O issue.","Watch the server log: the IOException is printed with the SECURITY message."],"tags":["security","git","path-traversal","symlink","filesystem","java"],"backgroundTag":null,"analyzedSha":"8cd9021c24cdbea1c3c12c966073708e83db60c2","analyzedAt":"2026-08-12T22:14:19.293Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}