appsmithorg/appsmith · critical · AppsmithPluginException

PE-PLG-5000

PE-PLG-5000

Error message

SECURITY: Unable to resolve real path for {} while validating Git root containment

What it means

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.

Source

Thrown at app/server/appsmith-git/src/main/java/com/appsmith/git/files/FileUtilsCEImpl.java:374

        if (!normalizedTarget.startsWith(gitRoot)) {
            throwPathTraversal(normalizedTarget, gitRoot);
        }

        // 2. Symlink-aware containment check (GHSA-fqwc-g9wm-5895) — blocks symbolic links committed
        // inside a repository that point outside the Git root. Path.normalize() above is purely
        // lexical and does NOT resolve symlinks, whereas every downstream file I/O sink follows
        // them. Resolve the real (symlink-free) path before comparing. Fails closed on I/O error.
        try {
            Path realGitRoot = toRealPathResolvingExistingPrefix(gitRoot);
            Path realTarget = toRealPathResolvingExistingPrefix(normalizedTarget);
            if (!realTarget.startsWith(realGitRoot)) {
                throwPathTraversal(realTarget, realGitRoot);
            }
        } catch (IOException e) {
            String errorMessage = "SECURITY: Unable to resolve real path for " + normalizedTarget
                    + " while validating Git root containment";
            log.error(errorMessage, e);
            throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, errorMessage);
        }
    }

    private void throwPathTraversal(Path attemptedPath, Path gitRoot) {
        String errorMessage = "SECURITY: Path traversal detected. Attempted to access " + attemptedPath
                + " which is outside the Git root " + gitRoot;
        log.error(errorMessage);
        throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, errorMessage);
    }

    /**
     * Resolves symbolic links in the longest existing prefix of {@code path} and re-appends the
     * remaining (not-yet-created) path segments lexically. {@link Path#toRealPath} cannot be used
     * directly because it requires the whole path to exist, while file writes legitimately target
     * paths that do not exist yet. By resolving the deepest existing ancestor we still detect any
     * symlink along the existing portion (including when {@code path} itself is a symlink) while
     * supporting yet-to-be-created files.
     */

View on GitHub (pinned to 8cd9021c24)

Solutions

  1. Verify the configured git root path exists and is readable/executable by the Appsmith server user: ls -la <gitRoot>.
  2. Find and remove dangling symlinks inside the repo root: find <gitRoot> -xtype l.
  3. Check filesystem/container permissions for the volume holding the git root; remount with correct uid/gid.
  4. Re-run the git operation after fixing permissions; the check fails closed so it will pass once resolution succeeds.
  5. Inspect the server log: the IOException is logged with the SECURITY message and shows the exact path that failed to resolve.

Example fix

// before
# git root unreadable by appsmith user
sudo chown -R appsmith:appsmith /data/gitroot

// after
# readable + resolvable; containment check now succeeds
ls -la /data/gitroot  # confirms ownership and no dangling links
Defensive patterns

Strategy: validation

Validate before calling

# Pre-flight: confirm the git root and ancestors are readable/resolvable.
git_root="$(config_get_gitRootPath)"
[ -d "$git_root" ] || echo "git root missing"
find "$git_root" -xtype l -print        # list dangling symlinks
stat -c '%U:%G %a %n' "$git_root"       # check ownership/perm

Type guard

// Java caller-side guard before invoking FileUtils
Path root = Paths.get(config.getGitRootPath()).toAbsolutePath().normalize();
if (!Files.isDirectory(root) || !Files.isReadable(root)) {
  throw new IllegalStateException("Git root not readable: " + root);
}

Try / catch

try {
  fileUtils.validatePathIsWithinGitRoot(target);
} catch (AppsmithPluginException e) {
  if (e.getCode() == AppsmithPluginError.PLUGIN_ERROR) {
    log.error("Git root containment check failed (I/O); verify permissions/symlinks", e);
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of appsmithorg/appsmith@8cd9021c24 (2026-08-12). Data as JSON: /api/errors/6f3944d94b32fdb9. Report an issue: GitHub.