CVE-2026-40981

Information Exposure
Affects
Spring Cloud Config
in
Spring
No items found.
Versions
>=3.1.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
Exclamation circle icon
Patch Available

This Vulnerability has been fixed in the Never-Ending Support (NES) version offered by HeroDevs.

Overview

Spring Cloud Config is a server/client toolkit from the Spring Cloud project that externalizes application configuration into a central Config Server. Client apps fetch their configuration at startup over HTTP, and the server can be backed by any of several pluggable environment repositories: a Git repository, a Vault server, AWS Secrets Manager, JDBC, or, since Spring Cloud Config 3.1.0, Google Cloud Secret Manager (GSM). The GSM backend is enabled by setting spring.cloud.config.server.gcp-secret-manager.* properties on the Config Server, which then exposes Secret Manager entries as PropertySource values to Config Clients.

A high-severity vulnerability (CVE-2026-40981) has been identified in the GSM backend's project-resolution logic. The vulnerable GoogleSecretManagerV1AccessStrategy.getProjectId() reads the GCP project ID directly from the client-supplied X-Project-ID HTTP header with no allow-list, no scoping, and no server-side override. The Config Server then uses its own Secret Manager service-account credentials to list and read secrets from whatever project the caller named. When the Config Server is deployed with credentials that span multiple GCP projects (the typical multi-tenant Config Server deployment pattern), any caller of the Config Server can pivot those credentials onto a victim project they were never intended to see and exfiltrate every secret value the server's credentials can read in that project.

Per OWASP: "Access control enforces policy such that users cannot act outside of their intended permissions. Failures typically lead to unauthorized information disclosure, modification, or destruction of all data or performing a business function outside the user's limits." Allowing a Config Client to choose which GCP project the Config Server reads secrets from, with no enforcement that the client is in fact authorized for that project, is exactly that failure: the access-control policy that ought to bind "client app C may only see secrets from project P" is never expressed in code.

The CVSS v3.1 base score for this vulnerability is 7.5 (High) with vector AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N. The attack vector is Network (any caller that can reach the Config Server), attack complexity is Low (a single header sets the target project), no privileges and no user interaction are required, and the impact is Confidentiality-only and High (the entire contents of unintended GSM projects are exposed); there is no integrity or availability impact because the attacker can only read.

This issue affects Spring Cloud Config >=3.1.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

Vulnerability Info

The vulnerability is in GoogleSecretManagerV1AccessStrategy in the spring-cloud-config-server module, specifically in the private method getProjectId() and the two public methods that consume its return value, getSecrets() and checkRemotePermissions().

An application is vulnerable when all of the following are true:

  • The application is running Spring Cloud Config Server 3.1.0 or later, on one of the affected version ranges above
  • The Config Server has the Google Secret Manager backend configured (spring.cloud.config.server.gcp-secret-manager.* properties present, GSM service account credentials available to the JVM)
  • The Config Server's GSM credentials have read access to more than one GCP project (the typical case for a shared Config Server)
  • The attacker can issue Config Client requests to the Config Server (network reachability and, when token-mandatory=true, a secretmanager.versions.access permission on a project they legitimately own)

When those conditions are met, the attacker chooses any GCP project name they wish to read by setting the X-Project-ID HTTP header on a normal Config Client request, and the Config Server returns the contents of that project's Secret Manager as PropertySource entries.

Prior to the fix, project resolution looked like this (pre-fix code, tagged at v4.3.2):

public class GoogleSecretManagerV1AccessStrategy implements GoogleSecretManagerAccessStrategy {

    @Override
    public List<Secret> getSecrets() {
        // Build the parent name.
        ProjectName project = ProjectName.of(getProjectId());

        ListSecretsRequest listSecretRequest = ListSecretsRequest.newBuilder()
            .setParent(project.toString())
            .build();
        // ... uses the server's own SecretManagerServiceClient credentials ...
    }

    public Boolean checkRemotePermissions() {
        // ... uses the client-supplied X-Config-Token to call testIamPermissions
        //     against getProjectId(): the SAME attacker-chosen project ...
        TestIamPermissionsResponse testIamPermissionsResponse = service.projects()
            .testIamPermissions(getProjectId(), requestBody)
            .execute();
        // ...
    }

    /**
     * @return the Project Id.
     */
    private String getProjectId() {
        String result = null;
        try {
            result = configProvider.getValue(HttpHeaderGoogleConfigProvider.PROJECT_ID_HEADER, true);
        }
        catch (Exception e) {
            // not in GCP
            HttpEntity<String> entity = new HttpEntity<String>("parameters", getMetadataHttpHeaders());
            result = rest.exchange(GoogleSecretManagerEnvironmentProperties.GOOGLE_METADATA_PROJECT_URL,
                    HttpMethod.GET, entity, String.class).getBody();
        }
        return result;
    }
}

The control-flow consequences of this implementation are the heart of the bug. configProvider.getValue(PROJECT_ID_HEADER, true) returns the value of the X-Project-ID HTTP header on the current Config Client request, with required=true so a missing header throws and falls into the catch block that queries the GCE metadata server. This means three things at once:

  1. The header is the first source consulted, ahead of any server-side configuration. There is no server-side property that lets an operator pin the GSM backend to a specific project.
  2. There is no allow-list. Whatever project name the caller writes into X-Project-ID is what the server uses, even if the server's credentials are intended for a completely different project.
  3. The server reads the secrets with its own GSM credentials (SecretManagerServiceClient.create() constructed from a server-side service account or the platform default). The client never proves they are authorized for the project they named; the server's credentials simply get retargeted at it.

When spring.cloud.config.server.gcp-secret-manager.token-mandatory is true (the default), the server additionally calls checkRemotePermissions() before loading any secrets. But that check uses testIamPermissions against the same attacker-chosen getProjectId() value, asking only "does this X-Config-Token have secretmanager.versions.access on the project the client just named." Any caller who has that permission on a project they legitimately use, including a low-privilege internal user with their own throwaway GCP project, can pass the check. Then the server's own credentials, not the caller's token, are used to read the secrets, so the caller harvests every secret in the named project that the server can see, not just the ones their token is authorized for. With token-mandatory=false, even this fig leaf is removed and unauthenticated callers can pivot the server's credentials onto any project name whatsoever.

The Config Server's threat model assumes that confidential property sources are isolated from clients that should not see them; the Git, Vault, and JDBC backends enforce this by either keying off server-side configuration or by passing through the client identity to the underlying store. The GSM backend, prior to this fix, did neither.

The fix extracts project resolution into a new helper class GcpProjectResolutionSupport and introduces two new configuration properties:

  • spring.cloud.config.server.gcp-secret-manager.allowed-project-ids (List<String>, default empty): when token-mandatory=false, the only project IDs that may appear in X-Project-ID. An empty list means client-supplied project IDs are rejected.
  • spring.cloud.config.server.gcp-secret-manager.project-id (String, default unset): a server-administered fallback project to use when X-Project-ID is absent and the GCE metadata server is unavailable (for example, local development).

The new resolver enforces the boundary that was missing:

public String resolve(GoogleConfigProvider configProvider, RestTemplate rest) {
    String headerValue = null;
    try {
        headerValue = configProvider.getValue(HttpHeaderGoogleConfigProvider.PROJECT_ID_HEADER, false);
    }
    catch (IllegalStateException ex) {
        logger.debug("No HttpServletRequest; cannot resolve GCP project for Secret Manager");
        return null;
    }
    if (StringUtils.hasText(headerValue)) {
        String projectId = headerValue.trim();
        if (properties.getTokenMandatory() || isClientProjectAllowed(projectId, properties)) {
            return projectId;
        }
        logger.warn("Rejecting Secret Manager access for disallowed X-Project-ID: " + projectId);
        return null;
    }
    String metadataProject = fetchProjectFromMetadata(rest);
    if (StringUtils.hasText(metadataProject)) {
        return metadataProject.trim();
    }
    if (StringUtils.hasText(properties.getProjectId())) {
        return properties.getProjectId().trim();
    }
    return null;
}

When the resolver returns null, both getSecrets() (now return Collections.emptyList()) and checkRemotePermissions() (now return Boolean.FALSE) short-circuit cleanly so no GSM-backed property sources are added for that request. The reference documentation under gcp-secret-manager-backend.adoc is updated with an explicit warning: "Setting token-mandatory to false allows clients to load secrets from any project in the allowed-project-ids list. It is recommended you never set token-mandatory to false so the permission check is performed before loading any secrets."

GoogleSecretManagerV1AccessStrategy's old constructors that did not take a GcpProjectResolutionSupport are retained but marked @Deprecated(forRemoval = true), and the in-tree GoogleSecretManagerEnvironmentRepositoryTests and a new GcpProjectResolutionSupportTests cover the four resolution paths (empty allow-list rejects, populated allow-list accepts, token-mandatory=true accepts the header without an allow-list because the IAM check is the gate, and metadata or project-id fallback when no header is present).

The Google Secret Manager backend was first added in March 13, 2020, which shipped in Spring Cloud Config 3.1.0. The vulnerable getProjectId() body was present in that first commit and remained essentially unchanged through to the pre-fix tip, so every Spring Cloud Config 3.1.x, 4.x, and 5.0.x release prior to the listed fix versions that has the GSM backend configured is affected. Spring Cloud Config 3.0.x and earlier do not contain the GSM backend at all and are not in scope.

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).
  • As an interim hardening step on a vulnerable deployment, set spring.cloud.config.server.gcp-secret-manager.token-mandatory=true (the default) so that the IAM permission check on X-Config-Token is at least applied. This does not close the cross-project access path on its own (the IAM check uses the same attacker-chosen project) but it does prevent unauthenticated callers from exercising it. The only complete fix is the patch.
  • Leverage a commercial support partner like HeroDevs for post-EOL security support through Never-Ending Support (NES) for Spring Cloud Config.
Vulnerability Details
Severity
Level
CVSS Assessment
Low
>=0 <4
Medium
>=4 <6
High
>=6 <8
Critical
>=8 <10
High
ID
CVE-2026-40981
PROJECT Affected
Spring Cloud Config
Versions Affected
>=3.1.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
NES Versions Affected
Published date
May 7, 2026
≈ Fix date
April 21, 2026
Category
Information Exposure
Vex Document
Download VEXHow do I use it?
Sign up for the latest vulnerability alerts fixed in
NES for Spring
Rss feed icon
Subscribe via RSS
or

By clicking “submit” I acknowledge receipt of our Privacy Policy.

Thanks for signing up for our Newsletter! We look forward to connecting with you.
Oops! Something went wrong while submitting the form.