VYPR
Low severityNVD Advisory· Published Oct 19, 2022· Updated May 8, 2025

CVE-2022-43411

CVE-2022-43411

Description

Jenkins GitLab Plugin 1.5.35 and earlier uses a non-constant time comparison function when checking whether the provided and expected webhook token are equal, potentially allowing attackers to use statistical methods to obtain a valid webhook token.

AI Insight

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

Jenkins GitLab Plugin ≤1.5.35 uses a non-constant time comparison for webhook tokens, enabling statistical token recovery attacks.

The Jenkins GitLab Plugin versions 1.5.35 and earlier contain a timing vulnerability in the webhook token validation mechanism. Instead of using a constant-time comparison function, the plugin performs a non-constant time equality check between the provided and expected webhook token [1][4]. This implementation flaw leaks information about the token through measurable differences in response timing based on character-by-character matches.

An attacker can exploit this weakness by sending a large number of crafted webhook requests while observing the response times. Statistical analysis of the timing variations allows the attacker to incrementally guess each character of the valid token [1][3]. The attack does not require prior authentication, as webhook endpoints are typically accessible to anyone who can reach the Jenkins instance. The primary prerequisite is network access to trigger the webhook endpoint and the ability to collect precise timing data.

Successful token recovery enables the attacker to forge valid webhook requests, potentially injecting malicious pipeline executions or triggering unauthorized build actions in Jenkins. This could lead to arbitrary code execution within the Jenkins environment, depending on the job configurations tied to the webhook [1][4].

The vulnerability is addressed in GitLab Plugin version 1.5.36, which replaces the non-constant time comparison with a constant-time alternative [3]. Users are strongly advised to upgrade to this version or later. No workarounds are provided for earlier versions, making patching the primary mitigation.

AI Insight generated on May 21, 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
org.jenkins-ci.plugins:gitlab-pluginMaven
< 1.5.361.5.36

Affected products

3

Patches

1
882f84c6a42b

[SECURITY-2877]

https://github.com/jenkinsci/gitlab-pluginMark WaiteOct 6, 2022via ghsa
2 files changed · +187 4
  • src/main/java/com/dabsquared/gitlabjenkins/webhook/build/BuildWebHookAction.java+38 4 modified
    @@ -1,10 +1,14 @@
     package com.dabsquared.gitlabjenkins.webhook.build;
     
    +import java.nio.charset.StandardCharsets;
    +import java.security.MessageDigest;
    +import java.security.NoSuchAlgorithmException;
     import java.util.logging.Logger;
     
    +import edu.umd.cs.findbugs.annotations.NonNull;
    +
     import hudson.model.Item;
     import hudson.model.Job;
    -import hudson.security.Messages;
     import hudson.security.Permission;
     import hudson.util.HttpResponses;
     import jenkins.model.Jenkins;
    @@ -34,21 +38,51 @@ public final void execute(StaplerResponse response) {
         protected abstract static class TriggerNotifier implements Runnable {
     
             private final Item project;
    -        private final String secretToken;
    +        private final byte[] hashedSecretToken;
             private final Authentication authentication;
     
             public TriggerNotifier(Item project, String secretToken, Authentication authentication) {
                 this.project = project;
    -            this.secretToken = secretToken;
    +            /* secretToken may be null, but we want constant time comparison of tokens */
    +            /* Remember secretToken was passed as null, then handle it as non-matchinng later */
    +            this.hashedSecretToken = secretToken != null ? hashedBytes(secretToken) : null;
                 this.authentication = authentication;
             }
     
    +        @NonNull
    +        private static byte[] hashedBytes(@NonNull String token) {
    +            final String HASH_ALGORITHM = "SHA-256";
    +            try {
    +                MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM);
    +                return digest.digest(token.getBytes(StandardCharsets.UTF_8));
    +            } catch (NoSuchAlgorithmException e) {
    +                throw new AssertionError("Hash algorithm " + HASH_ALGORITHM + " not found", e);
    +            }
    +        }
    +
    +        /* Constant time comparison of token argument and secretToken that was
    +         * passed to the constructor. If a null secretToken was passed to the
    +         * constructor, this method must still perform constant time comparison.
    +         */
    +        private boolean tokenMatches(@NonNull String token) {
    +            byte[] tokenBytes = hashedBytes(token);
    +            if (hashedSecretToken != null) {
    +                return MessageDigest.isEqual(tokenBytes, hashedSecretToken);
    +            }
    +
    +            // assure the isEqual comparison compares same number of bytes
    +            byte [] secretTokenBytes = tokenBytes.clone();
    +            // change last byte to assure the isEqual comparison will not match
    +            secretTokenBytes[secretTokenBytes.length - 1] ^= 1 << 3;
    +            return MessageDigest.isEqual(tokenBytes, secretTokenBytes);
    +        }
    +
             public void run() {
                 GitLabPushTrigger trigger = GitLabPushTrigger.getFromJob((Job<?, ?>) project);
                 if (trigger != null) {
                     if (StringUtils.isEmpty(trigger.getSecretToken())) {
                         checkPermission(Item.BUILD, project);
    -                } else if (!StringUtils.equals(trigger.getSecretToken(), secretToken)) {
    +                } else if (!tokenMatches(trigger.getSecretToken())) {
                         throw HttpResponses.errorWithoutStack(401, "Invalid token");
                     }
                     performOnPost(trigger);
    
  • src/test/java/com/dabsquared/gitlabjenkins/webhook/build/BuildWebHookActionTest.java+149 0 added
    @@ -0,0 +1,149 @@
    +/*
    + * Test tokenMatches in the BuildWebHookAction class
    + * Author: Mark Waite
    + */
    +package com.dabsquared.gitlabjenkins.webhook.build;
    +
    +import com.dabsquared.gitlabjenkins.connection.GitLabConnectionConfig;
    +import com.dabsquared.gitlabjenkins.GitLabPushTrigger;
    +
    +import edu.umd.cs.findbugs.annotations.NonNull;
    +
    +import hudson.model.FreeStyleProject;
    +import hudson.model.Item;
    +import hudson.model.Project;
    +import hudson.security.ACL;
    +
    +import org.acegisecurity.Authentication;
    +
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.junit.Rule;
    +import org.jvnet.hudson.test.JenkinsRule;
    +
    +import org.kohsuke.stapler.HttpResponses.HttpResponseException;
    +
    +import static org.junit.Assert.assertFalse;
    +import static org.junit.Assert.assertThrows;
    +import static org.junit.Assert.assertTrue;
    +
    +/**
    + * Test the BuildWebHookAction class
    + *
    + * @author Mark Waite
    + */
    +public class BuildWebHookActionTest {
    +
    +    @Rule
    +    public JenkinsRule j = new JenkinsRule();
    +
    +    private FreeStyleProject project;
    +    private GitLabPushTrigger trigger;
    +
    +    public BuildWebHookActionTest() {
    +    }
    +
    +    @Before
    +    public void confgureGitLabConnection() throws Exception {
    +        j.get(GitLabConnectionConfig.class).setUseAuthenticatedEndpoint(true);
    +    }
    +
    +    @Before
    +    public void createFreeStyleProjectWithGitLabTrigger() throws Exception {
    +        project = j.createFreeStyleProject();
    +        trigger = new GitLabPushTrigger();
    +        project.addTrigger(trigger);
    +    }
    +
    +    // trigger token == action token, expected to succeed
    +    @Test
    +    public void testNotifierTokenMatches() throws Exception {
    +        String triggerToken = "testNotifierTokenMatches-token";
    +        trigger.setSecretToken(triggerToken);
    +        String actionToken = triggerToken;
    +        BuildWebHookActionImpl action = new BuildWebHookActionImpl(project, actionToken);
    +        action.runNotifier();
    +        assertTrue("performOnPost not called, token did not match?", action.performOnPostCalled);
    +    }
    +
    +    // trigger token != action token, expected to throw an exception
    +    @Test
    +    public void testNotifierTokenDoesNotMatchString() throws Exception {
    +        String triggerToken = "testNotifierTokenDoesNotMatchString-token";
    +        trigger.setSecretToken(triggerToken);
    +        String actionToken = triggerToken + "-no-match"; // Won't match
    +        BuildWebHookActionImpl action = new BuildWebHookActionImpl(project, actionToken);
    +        assertThrows(HttpResponseException.class,
    +                () -> {
    +                    action.runNotifier();
    +                }
    +        );
    +        assertFalse("performOnPost was called, unexpected token match?", action.performOnPostCalled);
    +    }
    +
    +    // trigger token != null action token, expected to throw an exception
    +    @Test
    +    public void testNotifierTokenDoesNotMatchNull() throws Exception {
    +        String triggerToken = "testNotifierTokenDoesNotMatchNull-token";
    +        trigger.setSecretToken(triggerToken);
    +        String actionToken = null;
    +        BuildWebHookActionImpl action = new BuildWebHookActionImpl(project, actionToken);
    +        assertThrows(HttpResponseException.class,
    +                () -> {
    +                    action.runNotifier();
    +                }
    +        );
    +        assertFalse("performOnPost was called, unexpected token match?", action.performOnPostCalled);
    +    }
    +
    +    // null trigger token != action token, expected to succeed
    +    @Test
    +    public void testNullNotifierTokenAllowsAccess() throws Exception {
    +        // String triggerToken = null;
    +        // trigger.setSecretToken(triggerToken);
    +        String actionToken = "testNullNotifierTokenAllowsAccess-token";
    +        BuildWebHookActionImpl action = new BuildWebHookActionImpl(project, actionToken);
    +        action.runNotifier();
    +        assertTrue("performOnPost not called, token did not match?", action.performOnPostCalled);
    +    }
    +
    +    public class BuildWebHookActionImpl extends BuildWebHookAction {
    +
    +        // Used for the assertion that tokenMatches() returned true
    +        public boolean performOnPostCalled = false;
    +
    +        private final MyTriggerNotifier myNotifier;
    +
    +        public BuildWebHookActionImpl() {
    +            myNotifier = new MyTriggerNotifier(null, null, null);
    +        }
    +
    +        public BuildWebHookActionImpl(@NonNull Project project, @NonNull String token) {
    +            myNotifier = new MyTriggerNotifier(project, token, ACL.SYSTEM);
    +        }
    +
    +        public void runNotifier() {
    +            myNotifier.run();
    +        }
    +
    +        public class MyTriggerNotifier extends TriggerNotifier {
    +
    +            public MyTriggerNotifier(Item project, String secretToken, Authentication authentication) {
    +                super(project, secretToken, authentication);
    +            }
    +
    +            @Override
    +            protected void performOnPost(GitLabPushTrigger trigger) {
    +                performOnPostCalled = true;
    +            }
    +        }
    +
    +        @Override
    +        public void processForCompatibility() {
    +        }
    +
    +        @Override
    +        public void execute() {
    +        }
    +    }
    +}
    

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

0

No linked articles in our index yet.