CVE-2026-41002
This Vulnerability has been fixed in the Never-Ending Support (NES) version offered by HeroDevs.
Overview
Spring Cloud Config is a centralized configuration management tool for distributed systems built on the Spring Framework. Its spring-cloud-config-server module ships several environment-repository backends, including a Git backend that clones a configured remote repository into a local working directory (spring.cloud.config.server.git.basedir) and an alternate file: mode that opens an existing on-disk Git repository directly. JGit performs the actual repository operations on top of those paths.
A high-severity vulnerability (CVE-2026-41002) has been identified in those file-system code paths. The pre-fix JGitEnvironmentRepository checks basedir for existence, recursively deletes its contents, then asks JGit to clone into the same configured path, re-resolving it from the user-provided File each time. A local actor with the ability to influence the file system at or near basedir can swap the leaf for a symbolic link in the window between the server's check and JGit's clone, causing the clone to land outside the intended directory tree. The same recursive-delete step uses FileUtils.delete(file, FileUtils.RECURSIVE), which follows directory symlinks, so a planted symlink inside basedir causes the cleanup phase to recursively delete files outside basedir. The file: URI mode has the same class of issue: a symbolic link at the repository root or at the inner .git entry is silently followed, redirecting JGit to a different on-disk repository than the one the server was configured to serve.
Per OWASP: "A path traversal attack (also known as directory traversal) aims to access files and directories that are stored outside the web root folder. By manipulating variables that reference files with 'dot-dot-slash (../)' sequences and its variations or by using absolute file paths, it may be possible to access arbitrary files and directories stored on file system." In this CVE the path component being abused is not a URL parameter but a symbolic link that the server's file-system code follows during a clone or open, with the same end result: file accesses escape the configured Git working directory.
The CVSS v3.1 base score for this vulnerability is 6.0 (High) with vector AV:L/AC:H/PR:H/UI:N/S:C/C:H/I:H/A:N. The attack requires a local foothold and high attack complexity (the attacker must win a race or place a symlink at the right moment) and high privileges (typically write access in or near basedir), but on success the scope changes and the attacker can achieve high confidentiality and integrity impact by reading and overwriting arbitrary files the Config Server account can touch outside its configured tree.
This issue affects Spring Cloud Config >=1.0.0 <=3.1.13, >=4.1.0 <=4.1.9, >=4.2.0 <=4.2.6, >=4.3.0 <=4.3.2, and >=5.0.0 <=5.0.2.
Details
Module Info
- Product: Spring Cloud Config
- Affected packages: spring-cloud-config-server
- Affected versions: >=1.0.0 <=3.1.13, >=4.1.0 <=4.1.9, >=4.2.0 <=4.2.6, >=4.3.0 <=4.3.2, >=5.0.0 <=5.0.2
- GitHub repository: https://github.com/spring-cloud/spring-cloud-config
- Published packages: https://central.sonatype.com/artifact/org.springframework.cloud/spring-cloud-config-server
- Package manager: Maven
- Fixed in:
- NES for Spring Cloud Config 3.0.x, 3.1.x, 4.1.x, 4.2.x
- OSS Spring Cloud Config 4.3.3, Spring Cloud Config 5.0.3
Vulnerability Info
The vulnerability is in JGitEnvironmentRepository in the spring-cloud-config-server module. When the configured Git URI is a remote (https://, ssh://, etc.), the server prepares spring.cloud.config.server.git.basedir and clones the remote into it. When the URI is a file: URI, the server opens an existing on-disk Git repository at that path. Both code paths perform their checks against a path string and then re-derive the underlying java.io.File/Path when handing off to JGit, leaving a window in which the configured path can resolve to a different on-disk object than it did at check time.
The pre-fix copyRepository() method (the entry point for the remote-clone flow) had this shape:
private synchronized Git copyRepository() throws IOException, GitAPIException {
deleteBaseDirIfExists();
getBasedir().mkdirs();
Assert.state(getBasedir().exists(), "Could not create basedir: " + getBasedir());
if (getUri().startsWith(FILE_URI_PREFIX)) {
return copyFromLocalRepository();
}
else {
return cloneToBasedir();
}
}
private Git cloneToBasedir() throws GitAPIException {
CloneCommand clone = this.gitFactory.getCloneCommandByCloneRepository()
.setURI(getUri())
.setDirectory(getBasedir());
...
}
The configured basedir is resolved fresh inside deleteBaseDirIfExists, again inside getBasedir().mkdirs(), again inside Assert.state(getBasedir().exists(), ...), and a final time inside setDirectory(getBasedir()). A local actor who can place or replace a symbolic link at the configured path between any two of those calls steers the clone into the link's target. The recursive-delete step compounds the problem: deleteBaseDirIfExists iterates getBasedir().listFiles() and, for each entry, calls JGit's FileUtils.delete(file, FileUtils.RECURSIVE), which traverses directory symlinks. A symlink planted as a child of basedir causes the delete to recurse into the link's target and remove its contents.
The pre-fix copyFromLocalRepository() method (used for file: URIs) had a similar weakness:
File remote = new UrlResource(StringUtils.cleanPath(getUri())).getFile();
Assert.state(remote.isDirectory(), "No directory at " + getUri());
File gitDir = new File(remote, ".git");
Assert.state(gitDir.exists(), "No .git at " + getUri());
Assert.state(gitDir.isDirectory(), "No .git directory at " + getUri());
git = this.gitFactory.getGitByOpen(remote);
isDirectory() and exists() on a java.io.File follow symbolic links. A symlink whose target is a real Git working directory satisfies both checks, and gitFactory.getGitByOpen(remote) then opens the link target rather than the configured path. The same applies to the inner .git: a symbolic link there can redirect JGit's repository open to an entirely different repository on the same host. A local actor who controls (or can swap) the file at the configured file: URI can therefore make the Config Server serve configuration from a repository other than the one the operator wired in.
The fix introduces explicit-validation helpers that all canonicalize paths with Path#toRealPath(LinkOption.NOFOLLOW_LINKS) and re-resolve the configured path immediately before invoking JGit. For the remote-clone flow, recreateSecureBasedirForClone() removes any prior leaf (deleting symlinks as links rather than recursing into their targets), then creates the leaf with Files.createDirectory(base) so that a concurrent local swap to a symlink at the same path surfaces as FileAlreadyExistsException.
The hardened cloneToBasedir(Path resolvedBasedir) re-resolves the configured path with toRealPath(LinkOption.NOFOLLOW_LINKS) and asserts it still equals the directory recreateSecureBasedirForClone returned, before calling clone.call(). If the path resolved differently in the interval, the operation aborts with "Local clone directory changed before clone (possible path substitution)".
For the file: URI flow, resolveValidatedLocalFileRepositoryRoot() rejects symbolic links at the repository root and at .git, canonicalizes both with NOFOLLOW_LINKS, and asserts both resolve to real directories. After JGit opens the repository, assertOpenedLocalRepositoryPathsUnderRoot reads repository.getDirectory() and repository.getWorkTree() back from JGit, canonicalizes them the same way, and asserts they startsWith the validated root. JGit cannot report a working directory or git directory that lies outside the configured URI's resolved root.
deleteBaseDirIfExists() is rewritten on top of Files.walkFileTree(base, EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, ...). The empty enum set explicitly excludes FileVisitOption.FOLLOW_LINKS, so a directory-symlink entry inside basedir is deleted as a link rather than recursed into, and a tripwire assertPathUnderBase guards each visitor call.
The vulnerable file-system primitives have been present since 2014. The advisory's listed range starting at 3.1.x reflects Spring's currently-supported scope, but older versions all the way back to 1.0.0 are affected.
Mitigation
Only recent versions of Spring Cloud Config receive community support and updates. Older versions have no publicly available fixes for this vulnerability.
Users of the affected components should apply one of the following mitigations:
- Upgrade to a currently supported version of Spring Cloud Config. The OSS fix ships in Spring Cloud Config 4.3.3 (4.3.x line) and 5.0.3 (5.0.x line).
- Leverage a commercial support partner like HeroDevs for post-EOL security support through Never-Ending Support (NES) for Spring Cloud Config.
Credits
- Yu Bao (PayPal) (finder)