VYPR
Critical severityNVD Advisory· Published May 14, 2025· Updated May 15, 2025

CVE-2025-47884

CVE-2025-47884

Description

Jenkins OpenID Connect Provider Plugin uses overridable environment variables for build ID tokens, allowing job-configuration attackers to impersonate trusted jobs and access external services.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Jenkins OpenID Connect Provider Plugin uses overridable environment variables for build ID tokens, allowing job-configuration attackers to impersonate trusted jobs and access external services.

Vulnerability

Overview

In Jenkins OpenID Connect Provider Plugin 96.vee8ed882ec4d and earlier, the generation of build ID Tokens uses environment variables that can be overridden. The default claim template for the sub (Subject) claim uses JOB_URL environment variable. This design allows attackers with job configuration permissions to manipulate these values, especially when combined with other plugins like Environment Injector Plugin that permit arbitrary environment variable overrides [3].

Attack

Vector

Attackers who can configure jobs in Jenkins can craft a build ID Token that impersonates a trusted job by overriding environment variables used in claim templates. This requires the presence of certain other plugins that allow environment variable modification. The attack does not require authentication to external services directly but leverages the plugin's trust model, where external services rely on the integrity of the token's claims [1][2].

Impact

Successful exploitation allows the attacker to impersonate a trusted job, potentially gaining unauthorized access to external services that rely on these ID tokens for authentication (e.g., AWS, GCP, or secret stores like Vault). This could lead to privilege escalation, data exfiltration, or resource manipulation in the external service [3].

Mitigation

The vulnerability is fixed in OpenID Connect Provider Plugin version 111.v29fd614b_3617. Users should upgrade immediately. There are no known workarounds, as the security issue stems from the core token generation logic [3][4].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
io.jenkins.plugins:oidc-providerMaven
< 111.v29fd614b_3617111.v29fd614b_3617

Affected products

2

Patches

1
29fd614b3617

[SECURITY-3574]

3 files changed · +132 1
  • pom.xml+4 0 modified
    @@ -42,6 +42,10 @@
                 <groupId>org.jenkins-ci.plugins</groupId>
                 <artifactId>plain-credentials</artifactId>
             </dependency>
    +        <dependency>
    +            <groupId>org.jenkins-ci.plugins.workflow</groupId>
    +            <artifactId>workflow-api</artifactId>
    +        </dependency>
             <dependency>
                 <groupId>org.jenkins-ci.plugins</groupId>
                 <artifactId>credentials-binding</artifactId>
    
  • src/main/java/io/jenkins/plugins/oidc_provider/IdTokenCredentials.java+61 1 modified
    @@ -29,11 +29,18 @@
     import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
     import edu.umd.cs.findbugs.annotations.CheckForNull;
     import edu.umd.cs.findbugs.annotations.NonNull;
    +import hudson.EnvVars;
     import hudson.ExtensionList;
     import hudson.Util;
    +import hudson.model.AbstractBuild;
    +import hudson.model.Computer;
    +import hudson.model.EnvironmentContributingAction;
    +import hudson.model.EnvironmentContributor;
    +import hudson.model.Job;
     import hudson.model.Run;
     import hudson.model.TaskListener;
     import hudson.util.FormValidation;
    +import hudson.util.LogTaskListener;
     import hudson.util.Secret;
     import io.jenkins.plugins.oidc_provider.config.ClaimTemplate;
     import io.jenkins.plugins.oidc_provider.config.IdTokenConfiguration;
    @@ -53,6 +60,7 @@
     import java.security.spec.RSAPublicKeySpec;
     import java.time.Instant;
     import java.time.temporal.ChronoUnit;
    +import java.util.ArrayList;
     import java.util.Arrays;
     import java.util.Base64;
     import java.util.Collections;
    @@ -64,16 +72,22 @@
     import java.util.Set;
     import java.util.concurrent.atomic.AtomicBoolean;
     import java.util.function.Consumer;
    +import java.util.logging.Level;
    +import java.util.logging.Logger;
    +import java.util.stream.Collectors;
     import jenkins.model.Jenkins;
     import net.sf.json.JSONArray;
     import net.sf.json.JSONObject;
    +import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
     import org.kohsuke.stapler.DataBoundSetter;
     import org.kohsuke.stapler.HttpResponses;
     import org.kohsuke.stapler.QueryParameter;
     import org.kohsuke.stapler.StaplerRequest2;
     
     public abstract class IdTokenCredentials extends BaseStandardCredentials {
     
    +    private static final Logger LOGGER = Logger.getLogger(IdTokenCredentials.class.getName());
    +
         private static final long serialVersionUID = 1;
     
         /**
    @@ -192,7 +206,14 @@ RSAPublicKey publicKey() {
             Map<String, String> env;
             if (build != null) {
                 try {
    -                env = build.getEnvironment(TaskListener.NULL);
    +                TaskListener listener = new LogTaskListener(Logger.getLogger(IdTokenCredentials.class.getName()), Level.INFO);
    +                if (build instanceof FlowExecutionOwner.Executable feoe) {
    +                    var feo = feoe.asFlowExecutionOwner();
    +                    if (feo != null) {
    +                        listener = feo.getListener();
    +                    }
    +                }
    +                env = getEnvironment(build, listener);
                 } catch (IOException | InterruptedException x) {
                     throw new RuntimeException(x);
                 }
    @@ -225,6 +246,45 @@ RSAPublicKey publicKey() {
                 compact();
         }
     
    +    /**
    +     * Safer version of {@link Run#getEnvironment(TaskListener)} (and {@link Job#getEnvironment}) which prevents overrides.
    +     */
    +    private static EnvVars getEnvironment(Run<?, ?> build, TaskListener listener) throws IOException, InterruptedException {
    +        var envs = new ArrayList<EnvVars>();
    +        var c = Computer.currentComputer();
    +        if (c != null) {
    +            envs.add(c.getEnvironment());
    +            envs.add(c.buildEnvironment(listener));
    +        }
    +        envs.add(new EnvVars("CLASSPATH", ""));
    +        envs.add(build.getCharacteristicEnvVars()); // includes Job.getCharacteristicEnvVars
    +        for (var ec : EnvironmentContributor.all()) {
    +            var env = new EnvVars();
    +            ec.buildEnvironmentFor(build.getParent(), env, listener);
    +            envs.add(env);
    +            env = new EnvVars();
    +            ec.buildEnvironmentFor(build, env, listener);
    +            envs.add(env);
    +        }
    +        if (!(build instanceof AbstractBuild)) {
    +            for (var a : build.getActions(EnvironmentContributingAction.class)) {
    +                var env = new EnvVars();
    +                a.buildEnvironment(build, env);
    +                envs.add(env);
    +            }
    +        }
    +        var merged = new EnvVars();
    +        envs.stream().flatMap(env -> env.entrySet().stream()).collect(Collectors.groupingBy(Map.Entry::getKey)).entrySet().stream().forEach(entry -> {
    +            var values = entry.getValue().stream().map(Map.Entry::getValue).collect(Collectors.toSet());
    +            if (values.size() == 1) {
    +                merged.put(entry.getKey(), values.iterator().next());
    +            } else {
    +                listener.error("Refusing to consider conflicting values " + values + " of " + entry.getKey() + " for " + build);
    +            }
    +        });
    +        return merged;
    +    }
    +
         protected @NonNull Issuer findIssuer() {
             Run<?, ?> context = build;
             if (context == null) {
    
  • src/test/java/io/jenkins/plugins/oidc_provider/IdTokenCredentialsTest.java+67 0 modified
    @@ -30,15 +30,21 @@
     import com.cloudbees.plugins.credentials.CredentialsProvider;
     import com.cloudbees.plugins.credentials.CredentialsScope;
     import com.cloudbees.plugins.credentials.domains.Domain;
    +import hudson.EnvVars;
    +import hudson.model.EnvironmentContributor;
    +import hudson.model.Job;
     import org.htmlunit.html.HtmlAnchor;
     import org.htmlunit.html.HtmlForm;
     import org.htmlunit.html.HtmlPage;
     import hudson.model.Result;
    +import hudson.model.Run;
    +import hudson.model.TaskListener;
     import io.jenkins.plugins.oidc_provider.config.BooleanClaimType;
     import io.jenkins.plugins.oidc_provider.config.IntegerClaimType;
     import io.jenkins.plugins.oidc_provider.config.StringClaimType;
     import io.jsonwebtoken.Claims;
     import io.jsonwebtoken.Jwts;
    +import java.io.IOException;
     import java.math.BigInteger;
     import java.time.Instant;
     import java.time.temporal.ChronoUnit;
    @@ -47,6 +53,7 @@
     import java.util.List;
     import java.util.concurrent.atomic.AtomicReference;
     import jenkins.model.Jenkins;
    +import static jenkins.test.RunMatchers.logContains;
     import org.junit.Test;
     import static org.hamcrest.Matchers.*;
     import static org.hamcrest.MatcherAssert.assertThat;
    @@ -56,13 +63,19 @@
     import org.jenkinsci.plugins.workflow.support.actions.EnvironmentAction;
     import static org.junit.Assert.assertEquals;
     import static org.junit.Assert.assertTrue;
    +import org.junit.ClassRule;
     import org.junit.Rule;
    +import org.jvnet.hudson.test.BuildWatcher;
    +import org.jvnet.hudson.test.Issue;
     import org.jvnet.hudson.test.JenkinsRule;
     import org.jvnet.hudson.test.JenkinsSessionRule;
     import org.jvnet.hudson.test.MockAuthorizationStrategy;
    +import org.jvnet.hudson.test.TestExtension;
     
     public class IdTokenCredentialsTest {
     
    +    @ClassRule public static final BuildWatcher bw = new BuildWatcher();
    +
         @Rule public JenkinsSessionRule rr = new JenkinsSessionRule();
     
         @Test public void persistence() throws Throwable {
    @@ -204,4 +217,58 @@ public class IdTokenCredentialsTest {
             });
         }
     
    +    @Issue("SECURITY-3574")
    +    @Test public void spoofedClaimsRunLevel() throws Throwable {
    +        rr.then(r -> {
    +            var c = new IdTokenStringCredentials(CredentialsScope.GLOBAL, "test", null);
    +            CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), c);
    +            var p = r.createProject(Folder.class, "dir").createProject(WorkflowJob.class, "p");
    +            p.setDefinition(new CpsFlowDefinition("withCredentials([string(variable: 'TOK', credentialsId: 'test')]) {env.TOK = TOK}", true));
    +            var b = r.buildAndAssertSuccess(p);
    +            var idToken = b.getAction(EnvironmentAction.class).getEnvironment().get("TOK");
    +            System.out.println(idToken);
    +            var claims = Jwts.parserBuilder().
    +                setSigningKey(c.publicKey()).
    +                build().
    +                parseClaimsJws(idToken).
    +                getBody();
    +            System.out.println(claims);
    +            assertEquals(/* p.getAbsoluteUrl() */ "${JOB_URL}", claims.getSubject());
    +            assertThat(b, logContains("Refusing to consider conflicting values"));
    +        });
    +    }
    +    @SuppressWarnings("rawtypes") // design flaw in Jenkins core
    +    @TestExtension("spoofedClaimsRunLevel") public static final class RunSpoofer extends EnvironmentContributor {
    +        @Override public void buildEnvironmentFor(Run r, EnvVars envs, TaskListener listener) throws IOException, InterruptedException {
    +            envs.put("JOB_URL", "https://bogus.com/");
    +        }
    +    }
    +
    +    @Issue("SECURITY-3574")
    +    @Test public void spoofedClaimsJobLevel() throws Throwable {
    +        rr.then(r -> {
    +            var c = new IdTokenStringCredentials(CredentialsScope.GLOBAL, "test", null);
    +            CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), c);
    +            var p = r.createProject(Folder.class, "dir").createProject(WorkflowJob.class, "p");
    +            p.setDefinition(new CpsFlowDefinition("withCredentials([string(variable: 'TOK', credentialsId: 'test')]) {env.TOK = TOK}", true));
    +            var b = r.buildAndAssertSuccess(p);
    +            var idToken = b.getAction(EnvironmentAction.class).getEnvironment().get("TOK");
    +            System.out.println(idToken);
    +            var claims = Jwts.parserBuilder().
    +                setSigningKey(c.publicKey()).
    +                build().
    +                parseClaimsJws(idToken).
    +                getBody();
    +            System.out.println(claims);
    +            assertEquals(/* p.getAbsoluteUrl() */ "${JOB_URL}", claims.getSubject());
    +            assertThat(b, logContains("Refusing to consider conflicting values"));
    +        });
    +    }
    +    @SuppressWarnings("rawtypes") // design flaw in Jenkins core
    +    @TestExtension("spoofedClaimsJobLevel") public static final class JobSpoofer extends EnvironmentContributor {
    +        @Override public void buildEnvironmentFor(Job j, EnvVars envs, TaskListener listener) throws IOException, InterruptedException {
    +            envs.put("JOB_URL", "https://bogus.com/");
    +        }
    +    }
    +
     }
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

1