The number one mistake of teams running Spring Cloud Config Server in production today is treating the pod filesystem as if it were theirs alone. Shared volume, basedir cloned by JGit, and a namespace neighbor that swaps the path for a symlink at just the right moment. The result is reading and writing files outside the expected directory.
That scenario became CVE-2026-41002 (CVSS 7.4), disclosed on 2026-05-06 and fixed in Spring Cloud Config 4.3.3 and 5.0.3. It is a TOCTOU flaw, and it lands right on the cloud-native architecture most of us use without a second thought. Let's break down what happens and how to lock it down today.
As a Tech Leader, I have seen this "the filesystem is mine" pattern take down more than one team. You spin up the Config Server, it clones the Git repository into a local directory, serves config to the microservice mesh, and nobody looks at it again. Until the day it does something you never asked for.
What Spring Cloud Config Server is and why it clones a repository
The Config Server is the central piece of distributed configuration in Java microservice architectures. Instead of each service loading its own application.yaml, they all fetch configuration from a single point, which usually reads from a Git repository. That gives you versioning, auditing, and config rollback for free.
Under the hood, the one doing this work is the JGitEnvironmentRepository. When a request comes in asking for a service's config, it makes sure a local copy of the repository exists in a working directory, the so-called basedir. If the directory already exists with leftovers from a previous run, it cleans it and clones again. Sounds harmless, right? The problem lives exactly in that "clean and clone".
According to the official Spring advisory, the JGit flow does three things in sequence: it checks the basedir, recursively deletes its content, and tells JGit to clone into the same re-resolved path. Three distinct operations on the same path, at different moments. Keep that sentence in mind, because that is where the attack happens.
// Simplified version of the vulnerable flow (check-delete-clone)
private void prepareBasedir(File basedir) throws IOException {
// 1) CHECK: the path exists and looks ok right now
if (basedir.exists()) {
// 2) USE (part 1): delete everything inside it
FileUtils.delete(basedir, FileUtils.RECURSIVE);
}
// 3) USE (part 2): clone into the re-resolved path
Git.cloneRepository()
.setURI("https://git.internal/configs.git")
.setDirectory(basedir)
.call();
}
Between step 1 (check) and step 3 (use) there is a time window. If during that interval someone manages to swap what the path points to, JGit will operate on something else without noticing. That gap between checking and using has a name: TOCTOU.
TOCTOU in practice: what Time-of-Check to Time-of-Use means
TOCTOU stands for Time-of-check to time-of-use. It is a class of security race condition where the program validates a resource at one instant and uses that resource at a later instant, assuming nothing changed in between. On a filesystem, "nothing changed" is a dangerous assumption.
The OWASP documentation on TOCTOU describes the classic pattern: you check that a file is safe, and before you open it an attacker swaps that file for a symbolic link pointing to /etc/shadow. When your code finally reads the "safe file", it is reading the attacker's target.
In the Config Server case, the attack goes like this: the basedir (for example, the leaf folder where the repo is cloned) is swapped for a symlink that points to a directory outside the Git working directory. Since the check already passed, the recursive delete and the clone follow the symlink and escape the basedir. That allows reading and overwriting arbitrary files on the pod filesystem, according to the process permissions.
# What the local attacker does during the time window
# (namespace neighbor, malicious init container, compromised process)
rm -rf /shared/config-basedir/repo
ln -s /etc/secrets /shared/config-basedir/repo
# Now JGit's delete + clone operate on /etc/secrets, not on the repo
Notice one important thing: the attacker needs local access to the filesystem where the basedir lives. "Local" does not mean "unlikely". In the Kubernetes world, local is anything that shares that volume.
And there is one more detail that usually confuses anyone who never exploited a race condition: "winning the race" does not require surgical luck. The attacker does not need to hit the exact millisecond on a single try. The Config Server redoes the clean-and-clone cycle every time it gets a refresh request or detects a change in the repository. The attacker just runs the swap in a loop and waits for one of the hundreds of windows that open throughout the day. That is the difference between "theoretically possible" and "a matter of time", and it is why TOCTOU is treated as a serious flaw even when the interval looks tiny.
Why Kubernetes turns this into a real problem
Here is the point most people forget. On a dedicated machine, the Config Server filesystem is its alone, and the local attack surface is small. On Kubernetes, we break that premise all the time without noticing. A volume shared between containers in the same pod, a PersistentVolume with ReadWriteMany, init containers, log sidecars, a mounted hostPath. Each of those is a neighbor with access to the same path.
When the Config Server basedir lands on a shared volume, any process writing to that volume can throw the symlink swap into the TOCTOU window. And the Config Server usually runs with generous permissions, because it needs to write to the basedir. Put it all together: shared filesystem, privileged process, and a race condition in the library. It is the perfect storm of a universal architecture mistake.
The wrong way, which I see all the time, is to mount a shared "work" volume for the Config Server to clone the repo into, because "that way the cache survives a restart". The right way is the opposite:
# WRONG way: basedir on a shared volume writable by many
volumes:
- name: config-work
persistentVolumeClaim:
claimName: shared-rwx-pvc # ReadWriteMany: many pods write here
# RIGHT way: ephemeral basedir, isolated and exclusive to the pod
volumes:
- name: config-work
emptyDir: {} # lives with the pod, nobody outside writes
How to fix CVE-2026-41002 the right way
The fix has two layers, and you need both. The first is obvious and mandatory: update the dependency. The second is architectural, and it is what separates whoever just applies a patch from whoever understands the flaw.
Layer 1, the upgrade. The fix is in Spring Cloud Config 4.3.3 (line 2023.x / Spring Boot 3.x) and 5.0.3 (line 2025.x / Spring Boot 4.x). If you manage versions through the Spring Cloud BOM, bump the release train. Bumping only the config server is not enough; keep the BOM consistent.
<!-- pom.xml: align the Spring Cloud release train that ships the fix -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2025.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Layer 2, filesystem isolation. Even with the patch, the best practice is to never leave the Config Server basedir in a path that third parties can write to. Force an ephemeral directory exclusive to the process, and validate that it is not a symlink before using it. The JVM itself gives you a tool for this with NIO, which knows how not to follow links:
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
public final class BasedirGuard {
// Defense in depth: reject a basedir that is a symlink or sits
// outside the pod's exclusive root. Do not trust the patch alone.
public static Path validate(Path exclusiveRoot, Path basedir) {
if (Files.isSymbolicLink(basedir)) {
throw new SecurityException("basedir cannot be a symlink: " + basedir);
}
Path real = basedir.toAbsolutePath().normalize();
if (!real.startsWith(exclusiveRoot.toAbsolutePath().normalize())) {
throw new SecurityException("basedir escaped the root: " + real);
}
return real;
}
}
And in the Config Server application.yaml, point the basedir to an ephemeral, exclusive path, never to the shared volume:
spring:
cloud:
config:
server:
git:
uri: "https://git.internal/configs.git"
basedir: "/tmp/config-repo" # emptyDir, exclusive to the pod
force-pull: true
Combine that with a securityContext that runs the pod as a non-root user with readOnlyRootFilesystem, leaving only the ephemeral basedir directory writable. That way, even if a new TOCTOU variant shows up tomorrow, the attack surface stays tiny.
# securityContext that closes the local attack category
securityContext:
runAsNonRoot: true
runAsUser: 10001
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
This combination is not exclusive to the Config Server. It is the same pattern I apply to any workload that writes to disk inside the cluster. The underlying lesson of CVE-2026-41002 is that a secure library plus a careless architecture still gives you a vulnerability. JGit trusted the path it received, and the path came from a place others could touch. Whenever your application trusts a filesystem resource that third parties control, you have a TOCTOU waiting to be born.
What changes in your day to day on platform
In practice, three things change starting today. First: CVE-2026-41002 has CVSS 7.4 and was reported by a PayPal engineer, so it is not theoretical, people are exercising the vector. If you run a Config Server on a version older than 4.3.3 or 5.0.3, it goes into your patch queue for this sprint, not the next one.
Second: it is worth a quick audit of where each Config Server basedir is landing. Look for spring.cloud.config.server.git.basedir pointing to a mounted volume, and for PVCs with ReadWriteMany wired to the config deployment. If you find one, switch it to emptyDir and move on.
When you are exposed:
- Config Server with a Git backend running in a pod with a shared volume or hostPath
- Spring Cloud Config version older than 4.3.3 (line 3.x) or 5.0.3 (line 4.x)
- Config process running as root with a writable root filesystem
When the risk is low (but patch anyway):
- Basedir on a pod-exclusive
emptyDir, with no neighbor writing to the path - Pod with
runAsNonRootandreadOnlyRootFilesystemenabled
Anyone who runs platform knows that defense in depth is not a luxury. The patch closes the known window. Filesystem isolation closes the entire category of flaw. Do both, the way seniors do.
Frequently asked questions about CVE-2026-41002
What exactly is CVE-2026-41002?
It is a TOCTOU vulnerability (security race condition) in Spring Cloud Config Server with a Git backend. The JGitEnvironmentRepository checks the basedir, deletes its content, and clones into the same path, opening a window where a local attacker swaps the path for a symlink and makes access escape the expected directory. CVSS 7.4, fixed in 4.3.3 and 5.0.3.
Do I need to worry if my Config Server does not use Git?
The flaw is in the Git backend flow (JGitEnvironmentRepository). If you use the native filesystem backend, Vault, or another one, this specific vector does not apply. Even so, updating the Spring Cloud release train is the standard recommendation, because it carries other fixes.
Does the symlink swap work from anywhere on the network?
No. The attacker needs local write access to the filesystem where the basedir lives. The risk gets high precisely on Kubernetes, where shared volumes and ReadWriteMany give that local access to other containers and pods. On a dedicated and isolated host, the exposure is much smaller.
Does just updating the version solve it?
The patch closes the known TOCTOU window and is mandatory. But treating the basedir as ephemeral and exclusive to the pod, rejecting symlinks, and running as non-root eliminates the entire category of local attack. The two layers together are what I recommend as a Tech Leader.
How do I test whether I am vulnerable without exploiting anything?
Check the version of spring-cloud-config-server resolved by your BOM and the value of spring.cloud.config.server.git.basedir. A version older than 4.3.3 or 5.0.3 plus a basedir on a shared volume equals confirmed risk. You do not need a PoC to prioritize the patch.
Takeaways and next steps
- TOCTOU is a security race condition: checking and using a resource at different instants assumes nothing changed, and on a filesystem that is false
- Kubernetes amplifies the risk: a shared volume and
ReadWriteManygive a neighbor the local access the attack needs - Fix it in two layers: move up to Spring Cloud Config 4.3.3 or 5.0.3 and isolate the basedir in an exclusive
emptyDir, rejecting symlinks
Have you ever run a Config Server with a shared volume without noticing this risk? Tell me in the comments how the configuration architecture looks on your stack. I want to know how many teams fell for the "the filesystem is mine" trap.
Next week I am opening the Java DevSecOps series showing how to tie together securityContext, readOnlyRootFilesystem, and Network Policies into a genuinely secure pod template for Spring Boot. If you run platform, that is the content that will become your production checklist. To not miss it, follow the Spring Boot and DevOps content on Meu Universo Nerd, check out the material on Spring Security and protecting endpoints in production, and the discussion on observability and security in Java microservices.