CVE-2022-36882
Description
A cross-site request forgery (CSRF) vulnerability in Jenkins Git Plugin 4.11.3 and earlier allows attackers to trigger builds of jobs configured to use an attacker-specified Git repository and to cause them to check out an attacker-specified commit.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CSRF in Jenkins Git Plugin before 4.11.4 allows attackers to trigger builds with an attacker-specified Git repository and commit, without authentication.
Vulnerability
Description
CVE-2022-36882 is a cross-site request forgery (CSRF) vulnerability in the Jenkins Git Plugin up to version 4.11.3. The plugin provides a webhook endpoint at /git/notifyCommit that can be used to notify Jenkins of changes to a Git repository. This endpoint, designed for basic functionality, accepts a repository URL and initiates a build of jobs configured to use that repository. The vulnerability allows an unauthenticated attacker to craft a malicious request that, when triggered by a victim with sufficient permissions (e.g., a Jenkins administrator or user with build trigger permissions), causes Jenkins to trigger a build of any job that is configured to use the attacker-specified Git repository and to check out an attacker-specified commit. The root cause is the lack of a CSRF token or other state-changing request validation on the /git/notifyCommit endpoint [1][2].
Exploitation
To exploit this vulnerability, an attacker needs to trick a Jenkins user who has access to the /git/notifyCommit endpoint into submitting a crafted request. This can be achieved through social engineering, such as embedding the malicious request in a link or image tag on a website visited by the victim. The attack does not require authentication to the Jenkins instance itself, but the victim must have the necessary permissions (e.g., Job/Build permission) for the affected jobs. The attacker must know the URL of the Jenkins server and the name of a job that uses a Git repository which the attacker can specify (or the attacker can supply a repository URL that matches one of the job's configured SCMs) [1][2].
Impact
A successful CSRF attack against the /git/notifyCommit endpoint enables the attacker to trigger builds of arbitrary jobs that are configured to use a Git repository. Moreover, the attacker can force the build to check out a specific commit, which could be malicious (e.g., containing backdoors or altered code). This could lead to supply chain compromise, where the attacker-controlled code is integrated into the software produced by the build. The CVSS score for this vulnerability is Medium [1][2][3].
Mitigation
The vulnerability is fixed in Git Plugin version 4.11.4. Users should upgrade to this version immediately. The fix adds CSRF protection (e.g., requiring a POST request with a valid CSRF token) to the /git/notifyCommit endpoint. Additionally, administrators can consider disabling the webhook endpoint or restricting access to it via Jenkins' global security settings if the endpoint is not needed [1][2][4].
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.
| Package | Affected versions | Patched versions |
|---|---|---|
org.jenkins-ci.plugins:gitMaven | < 4.11.4 | 4.11.4 |
Affected products
2- Jenkins project/Jenkins Git Pluginv5Range: unspecified
Patches
1b46165c74a0b[SECURITY-284][SECURITY-907]
13 files changed · +659 −35
README.adoc+15 −4 modified@@ -175,10 +175,10 @@ Refer to webhook documentation for your repository: * link:https://github.com/jenkinsci/gitea-plugin/blob/master/docs/README.md[Gitea] Other git repositories can use a link:https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks[post-receive hook] in the remote repository to notify Jenkins of changes. -Add the following line in your `hooks/post-receive` file on the git server, replacing <URL of the Git repository> with the fully qualified URL you use when cloning the repository. +Add the following line in your `hooks/post-receive` file on the git server, replacing <URL of the Git repository> with the fully qualified URL you use when cloning the repository, and replacing <Access token> with a token generated by a Jenkins administrator using the "Git plugin notifyCommit access tokens" section of the "Configure Global Security" page. .... -curl http://yourserver/git/notifyCommit?url=<URL of the Git repository> +curl http://yourserver/git/notifyCommit?url=<URL of the Git repository>&token=<Access token> .... This will scan all the jobs that: @@ -191,8 +191,19 @@ If polling finds a change worthy of a build, a build will be triggered. This allows a notify script to remain the same for all Jenkins jobs. Or if you have multiple repositories under a single repository host application (such as Gitosis), you can share a single post-receive hook script with all the repositories. -Finally, this URL doesn't require authentication even for secured Jenkins, because the server doesn't directly use anything that the client is sending. -It polls to verify that there is a change before it actually starts a build. + +The `token` parameter is required by default as a security measure, but can be disabled by the following link:https://www.jenkins.io/doc/book/managing/system-properties/[system property]: + +.... +hudson.plugins.git.GitStatus.NOTIFY_COMMIT_ACCESS_CONTROL +.... + +It has two modes: + +* `disabled-for-polling` - Allows unauthenticated requests as long as they only request polling of the repository supplied in the `url` query parameter. Prohibits unauthenticated requests that attempt to schedule a build immediately by providing a +`sha1` query parameter. +* `disabled` - Fully disables the access token mechanism and allows all requests to `notifyCommit` +to be unauthenticated. *This option is insecure and is not recommended.* When notifyCommit is successful, the list of triggered projects is returned.
src/main/java/hudson/plugins/git/ApiTokenPropertyConfiguration.java+183 −0 added@@ -0,0 +1,183 @@ +package hudson.plugins.git; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.Util; +import hudson.model.PersistentDescriptor; +import hudson.util.HttpResponses; +import jenkins.model.GlobalConfiguration; +import jenkins.model.GlobalConfigurationCategory; +import jenkins.model.Jenkins; +import net.jcip.annotations.GuardedBy; +import net.sf.json.JSONObject; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.interceptor.RequirePOST; + +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + + +@Extension +@Restricted(NoExternalUse.class) +@Symbol("apiTokenProperty") +public class ApiTokenPropertyConfiguration extends GlobalConfiguration implements PersistentDescriptor { + + private static final Logger LOGGER = Logger.getLogger(ApiTokenPropertyConfiguration.class.getName()); + private static final SecureRandom RANDOM = new SecureRandom(); + private static final String HASH_ALGORITHM = "SHA-256"; + + @GuardedBy("this") + private final List<HashedApiToken> apiTokens; + + public ApiTokenPropertyConfiguration() { + this.apiTokens = new ArrayList<>(); + } + + public static ApiTokenPropertyConfiguration get() { + return GlobalConfiguration.all().get(ApiTokenPropertyConfiguration.class); + } + + @NonNull + @Override + public GlobalConfigurationCategory getCategory() { + return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class); + } + + @RequirePOST + public HttpResponse doGenerate(StaplerRequest req) { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + + String apiTokenName = req.getParameter("apiTokenName"); + JSONObject json = this.generateApiToken(apiTokenName); + save(); + + return HttpResponses.okJSON(json); + } + + public JSONObject generateApiToken(@NonNull String name) { + byte[] random = new byte[16]; + RANDOM.nextBytes(random); + + String plainTextApiToken = Util.toHexString(random); + assert plainTextApiToken.length() == 32; + + String apiTokenValueHashed = Util.toHexString(hashedBytes(plainTextApiToken.getBytes(StandardCharsets.US_ASCII))); + HashedApiToken apiToken = new HashedApiToken(name, apiTokenValueHashed); + + synchronized (this) { + this.apiTokens.add(apiToken); + } + + JSONObject json = new JSONObject(); + json.put("uuid", apiToken.getUuid()); + json.put("name", apiToken.getName()); + json.put("value", plainTextApiToken); + + return json; + } + + @NonNull + private static byte[] hashedBytes(byte[] tokenBytes) { + MessageDigest digest; + try { + digest = MessageDigest.getInstance(HASH_ALGORITHM); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("There is no " + HASH_ALGORITHM + " available in this system", e); + } + return digest.digest(tokenBytes); + } + + @RequirePOST + public HttpResponse doRevoke(StaplerRequest req) { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + + String apiTokenUuid = req.getParameter("apiTokenUuid"); + if (StringUtils.isBlank(apiTokenUuid)) { + return HttpResponses.errorWithoutStack(400, "API token UUID cannot be empty"); + } + + synchronized (this) { + this.apiTokens.removeIf(apiToken -> apiToken.getUuid().equals(apiTokenUuid)); + } + save(); + + return HttpResponses.ok(); + } + + public synchronized Collection<HashedApiToken> getApiTokens() { + return Collections.unmodifiableList(new ArrayList<>(this.apiTokens)); + } + + public boolean isValidApiToken(String plainApiToken) { + if (StringUtils.isBlank(plainApiToken)) { + return false; + } + + return this.hasMatchingApiToken(plainApiToken); + } + + public synchronized boolean hasMatchingApiToken(@NonNull String plainApiToken) { + byte[] hash = hashedBytes(plainApiToken.getBytes(StandardCharsets.US_ASCII)); + return this.apiTokens.stream().anyMatch(apiToken -> apiToken.match(hash)); + } + + public static class HashedApiToken implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String uuid; + private final String name; + private final String hash; + + private HashedApiToken(String name, String hash) { + this.uuid = UUID.randomUUID().toString(); + this.name = name; + this.hash = hash; + } + + private HashedApiToken(String uuid, String name, String hash) { + this.uuid = uuid; + this.name = name; + this.hash = hash; + } + + public String getUuid() { + return uuid; + } + + public String getName() { + return name; + } + + public String getHash() { + return hash; + } + + private boolean match(byte[] hashedBytes) { + byte[] hashFromHex; + try { + hashFromHex = Util.fromHexString(hash); + } catch (NumberFormatException e) { + LOGGER.log(Level.INFO, "The API token with name=[{0}] is not in hex-format and so cannot be used", name); + return false; + } + + return MessageDigest.isEqual(hashFromHex, hashedBytes); + } + } +}
src/main/java/hudson/plugins/git/GitStatus.java+24 −5 modified@@ -12,21 +12,20 @@ import hudson.security.ACL; import hudson.security.ACLContext; import hudson.triggers.SCMTrigger; -import java.io.IOException; import java.io.PrintWriter; import java.net.URISyntaxException; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_OK; import jenkins.model.Jenkins; import jenkins.scm.api.SCMEvent; import jenkins.triggers.SCMTriggerItem; +import jenkins.util.SystemProperties; import org.apache.commons.lang.StringUtils; import static org.apache.commons.lang.StringUtils.isNotEmpty; @@ -39,6 +38,9 @@ */ @Extension public class GitStatus implements UnprotectedRootAction { + static /* not final */ String NOTIFY_COMMIT_ACCESS_CONTROL = + SystemProperties.getString(GitStatus.class.getName() + ".NOTIFY_COMMIT_ACCESS_CONTROL"); + @Override public String getDisplayName() { return "Git"; @@ -113,8 +115,25 @@ public String toString() { } public HttpResponse doNotifyCommit(HttpServletRequest request, @QueryParameter(required=true) String url, - @QueryParameter(required=false) String branches, - @QueryParameter(required=false) String sha1) throws ServletException, IOException { + @QueryParameter() String branches, @QueryParameter() String sha1, + @QueryParameter() String token) { + if (!"disabled".equalsIgnoreCase(NOTIFY_COMMIT_ACCESS_CONTROL) + && !"disabled-for-polling".equalsIgnoreCase(NOTIFY_COMMIT_ACCESS_CONTROL)) { + if (StringUtils.isEmpty(token)) { + return HttpResponses.errorWithoutStack(401, "An access token is required. Please refer to Git plugin documentation for details."); + } + if (!ApiTokenPropertyConfiguration.get().isValidApiToken(token)) { + return HttpResponses.errorWithoutStack(403, "Invalid access token"); + } + } + if ("disabled-for-polling".equalsIgnoreCase(NOTIFY_COMMIT_ACCESS_CONTROL) && StringUtils.isNotEmpty(sha1)) { + if (StringUtils.isEmpty(token)) { + return HttpResponses.errorWithoutStack(401, "An access token is required when using the sha1 parameter. Please refer to Git plugin documentation for details."); + } + if (!ApiTokenPropertyConfiguration.get().isValidApiToken(token)) { + return HttpResponses.errorWithoutStack(403, "Invalid access token"); + } + } lastURL = url; lastBranches = branches; if(StringUtils.isNotBlank(sha1)&&!SHA1_PATTERN.matcher(sha1.trim()).matches()){ @@ -197,7 +216,7 @@ private static String normalizePath(String path) { } /** - * Contributes to a {@link #doNotifyCommit(HttpServletRequest, String, String, String)} response. + * Contributes to a {@link #doNotifyCommit(HttpServletRequest, String, String, String, String)} response. * * @since 1.4.1 */
src/main/resources/hudson/plugins/git/ApiTokenPropertyConfiguration/config.jelly+54 −0 added@@ -0,0 +1,54 @@ +<?jelly escape-by-default='true'?> + +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form" xmlns:l="/lib/layout"> + <f:section title="${%Git plugin notifyCommit access tokens}"> + <st:adjunct includes="hudson.plugins.git.ApiTokenPropertyConfiguration.resources" /> + <f:entry title="${%Current access tokens}" help="${descriptor.getHelpFile('tokens')}"> + <div class="api-token-list"> + <j:set var="apiTokens" value="${instance.apiTokens}" /> + <div class="api-token-list-empty-item ${apiTokens == null || apiTokens.isEmpty() ? '' : 'hidden'}"> + <div class="list-empty-message">${%There are no access tokens yet.}</div> + </div> + <f:repeatable var="apiToken" items="${apiTokens}" minimum="0" add="${%Add new access token}"> + <j:choose> + <j:when test="${apiToken != null}"> + <input type="hidden" class="api-token-uuid-input" name="apiTokenUuid" value="${apiToken.uuid}" /> + <div class="api-token-list-item-row api-token-list-existing-token"> + <f:textbox readonly="true" value="${apiToken.name}" /> + <a href="#" onclick="return revokeApiToken(this)" class="yui-button api-token-revoke-button" + data-confirm="${%Are you sure you want to revoke this access token?}" + data-target-url="${descriptor.descriptorFullUrl}/revoke"> + ${%Revoke} + </a> + </div> + </j:when> + <j:otherwise> + <div class="api-token-list-item"> + <div class="api-token-list-item-row"> + <input type="hidden" class="api-token-uuid-input" name="apiTokenUuid" value="${apiToken.uuid}" /> + <f:textbox clazz="api-token-name-input" name="apiTokenName" placeholder="${%Access token name}"/> + <span class="new-api-token-value hidden"><!-- to be filled by JS --></span> + <span class="yui-button api-token-save-button"> + <button type="button" tabindex="0" data-target-url="${descriptor.descriptorFullUrl}/generate" onclick="saveApiToken(this)"> + ${%Generate} + </button> + </span> + <span class="api-token-cancel-button"> + <f:repeatableDeleteButton value="${%Cancel}" /> + </span> + <l:copyButton message="${%Copied}" text="" clazz="hidden" tooltip="${%Copy to clipboard}" /> + <a href="#" onclick="return revokeApiToken(this)" class="yui-button api-token-revoke-button hidden" + data-confirm="${%Are you sure you want to revoke this access token?}" + data-target-url="${descriptor.descriptorFullUrl}/revoke"> + ${%Revoke} + </a> + </div> + <span class="warning api-token-warning-message hidden">${%Access token will only be displayed once.}</span> + </div> + </j:otherwise> + </j:choose> + </f:repeatable> + </div> + </f:entry> + </f:section> +</j:jelly>
src/main/resources/hudson/plugins/git/ApiTokenPropertyConfiguration/help-tokens.html+15 −0 added@@ -0,0 +1,15 @@ +<div> + <p>These access tokens serve as a way of authenticating requests to the <code>notifyCommit</code> endpoint. + <p>By default, all requests to <code>notifyCommit</code> must include a valid token in the <code>token</code> query parameter. However, it is possible to disable + that requirement with the <a href="https://www.jenkins.io/doc/book/managing/system-properties/">system property</a>: + <pre><code>hudson.plugins.git.GitStatus.NOTIFY_COMMIT_ACCESS_CONTROL</code></pre> + <br/> + It has two modes: + <ul> + <li><code>disabled-for-polling</code> - Allows unauthenticated requests as long as they only request polling of the repository supplied in the + <code>url</code> query parameter. Prohibits unauthenticated requests that attempt to schedule a build immediately by providing a + <code>sha1</code> query parameter.</li> + <li><code>disabled</code> - Fully disables the access token mechanism and allows all requests to <code>notifyCommit</code> + to be unauthenticated. <b>This option is insecure and is not recommended.</b></li> + </ul> +</div>
src/main/resources/hudson/plugins/git/ApiTokenPropertyConfiguration/resources.css+18 −0 added@@ -0,0 +1,18 @@ +.api-token-list .api-token-list-item-row { + display: flex; + align-items: center; + max-width: 700px; +} +.api-token-list .api-token-list-item-row.api-token-list-existing-api-token { + justify-content: space-between; +} +.api-token-list .api-token-list-item .hidden, .api-token-list .api-token-list-empty-item.hidden { + display: none; +} + +.api-token-list .api-token-revoke-button, .api-token-list .new-api-token-value { + padding: 0 0.5rem; +} +.api-token-list .api-token-warning-message, .api-token-list .api-token-save-button { + margin: 0.5rem 0; +}
src/main/resources/hudson/plugins/git/ApiTokenPropertyConfiguration/resources.js+81 −0 added@@ -0,0 +1,81 @@ +function revokeApiToken(anchorRevoke) { + const repeatedChunk = anchorRevoke.up('.repeated-chunk'); + const apiTokenList = repeatedChunk.up('.api-token-list'); + const confirmMessage = anchorRevoke.getAttribute('data-confirm'); + const targetUrl = anchorRevoke.getAttribute('data-target-url'); + const inputUuid = repeatedChunk.querySelector('.api-token-uuid-input'); + const apiTokenUuid = inputUuid.value; + + if (confirm(confirmMessage)) { + new Ajax.Request(targetUrl, { + method: "post", + parameters: {apiTokenUuid: apiTokenUuid}, + onSuccess: function(res, _) { + repeatedChunk.remove(); + adjustEmptyListMessage(apiTokenList); + } + }); + } + + return false; +} + +function saveApiToken(button){ + if (button.hasClassName('request-pending')) { + // avoid multiple requests to be sent if user is clicking multiple times + return; + } + button.addClassName('request-pending'); + const targetUrl = button.getAttribute('data-target-url'); + const repeatedChunk = button.up('.repeated-chunk'); + const apiTokenList = repeatedChunk.up('.api-token-list'); + const nameInput = repeatedChunk.querySelector('.api-token-name-input'); + const apiTokenName = nameInput.value; + + new Ajax.Request(targetUrl, { + method: "post", + parameters: {apiTokenName: apiTokenName}, + onSuccess: function(res, _) { + const { name, value, uuid } = res.responseJSON.data; + nameInput.value = name; + + const apiTokenValueSpan = repeatedChunk.querySelector('.new-api-token-value'); + apiTokenValueSpan.innerText = value; + apiTokenValueSpan.removeClassName('hidden'); + + const apiTokenCopyButton = repeatedChunk.querySelector('.copy-button'); + apiTokenCopyButton.setAttribute('text', value); + apiTokenCopyButton.removeClassName('hidden'); + + const uuidInput = repeatedChunk.querySelector('.api-token-uuid-input'); + uuidInput.value = uuid; + + const warningMessage = repeatedChunk.querySelector('.api-token-warning-message'); + warningMessage.removeClassName('hidden'); + + // we do not want to allow user to create twice api token using same name by mistake + button.remove(); + + const revokeButton = repeatedChunk.querySelector('.api-token-revoke-button'); + revokeButton.removeClassName('hidden'); + + const cancelButton = repeatedChunk.querySelector('.api-token-cancel-button'); + cancelButton.addClassName('hidden'); + + repeatedChunk.addClassName('api-token-list-fresh-item'); + + adjustEmptyListMessage(apiTokenList); + } + }); +} + +function adjustEmptyListMessage(apiTokenList) { + const emptyListMessageClassList = apiTokenList.querySelector('.api-token-list-empty-item').classList; + + const apiTokenListLength = apiTokenList.querySelectorAll('.api-token-list-existing-item, .api-token-list-fresh-item').length; + if (apiTokenListLength >= 1) { + emptyListMessageClassList.add("hidden"); + } else { + emptyListMessageClassList.remove("hidden"); + } +}
src/test/java/hudson/plugins/git/AbstractGitProject.java+5 −0 modified@@ -59,6 +59,7 @@ import org.junit.Rule; import org.jvnet.hudson.test.CaptureEnvironmentBuilder; +import org.jvnet.hudson.test.FlagRule; import org.jvnet.hudson.test.JenkinsRule; /** @@ -70,6 +71,10 @@ public class AbstractGitProject extends AbstractGitRepository { @Rule public JenkinsRule jenkins = new JenkinsRule(); + @Rule + public FlagRule<String> notifyCommitAccessControl = + new FlagRule<>(() -> GitStatus.NOTIFY_COMMIT_ACCESS_CONTROL, x -> GitStatus.NOTIFY_COMMIT_ACCESS_CONTROL = x); + protected FreeStyleProject setupProject(List<BranchSpec> branches, boolean authorOrCommitter) throws Exception { FreeStyleProject project = jenkins.createFreeStyleProject(); GitSCM scm = new GitSCM(remoteConfigs(), branches,
src/test/java/hudson/plugins/git/GitStatusCrumbExclusionTest.java+17 −3 modified@@ -39,7 +39,10 @@ public class GitStatusCrumbExclusionTest { private final String branchArgument; private final byte[] branchArgumentBytes; - public GitStatusCrumbExclusionTest() throws IOException { + private final String notifyCommitApiToken; + private final byte[] notifyCommitApiTokenBytes; + + public GitStatusCrumbExclusionTest() throws Exception { String jenkinsUrl = r.getURL().toExternalForm(); if (!jenkinsUrl.endsWith("/")) { jenkinsUrl = jenkinsUrl + "/"; @@ -56,6 +59,9 @@ public GitStatusCrumbExclusionTest() throws IOException { branchArgument = "branches=origin/some-branch-name"; branchArgumentBytes = branchArgument.getBytes(StandardCharsets.UTF_8); + + notifyCommitApiToken = "token=" + ApiTokenPropertyConfiguration.get().generateApiToken("test").getString("value"); + notifyCommitApiTokenBytes = notifyCommitApiToken.getBytes(StandardCharsets.UTF_8); } private HttpURLConnection connectionPOST; @@ -86,6 +92,8 @@ public void testPOSTValidPathNoArgument() throws Exception { public void testPOSTValidPathMandatoryArgument() throws Exception { try (OutputStream os = connectionPOST.getOutputStream()) { os.write(urlArgumentBytes); + os.write(separatorBytes); + os.write(notifyCommitApiTokenBytes); } assertThat(connectionPOST.getResponseCode(), is(HttpURLConnection.HTTP_OK)); assertThat(connectionPOST.getResponseMessage(), is("OK")); @@ -97,6 +105,8 @@ public void testPOSTValidPathEmptyMandatoryArgument() throws Exception { String urlEmptyArgument = "url="; // Empty argument is not a valid URL byte[] urlEmptyArgumentBytes = urlEmptyArgument.getBytes(StandardCharsets.UTF_8); os.write(urlEmptyArgumentBytes); + os.write(separatorBytes); + os.write(notifyCommitApiTokenBytes); } assertThat(connectionPOST.getResponseCode(), is(HttpURLConnection.HTTP_BAD_REQUEST)); assertThat(connectionPOST.getResponseMessage(), is("Bad Request")); @@ -108,6 +118,8 @@ public void testPOSTValidPathBadURLInMandatoryArgument() throws Exception { String urlBadArgument = "url=" + "http://256.256.256.256/"; // Not a valid URI per Java 8 javadoc byte[] urlBadArgumentBytes = urlBadArgument.getBytes(StandardCharsets.UTF_8); os.write(urlBadArgumentBytes); + os.write(separatorBytes); + os.write(notifyCommitApiTokenBytes); } assertThat(connectionPOST.getResponseCode(), is(HttpURLConnection.HTTP_OK)); assertThat(connectionPOST.getResponseMessage(), is("OK")); @@ -119,6 +131,8 @@ public void testPOSTValidPathMandatoryAndOptionalArgument() throws Exception { os.write(urlArgumentBytes); os.write(separatorBytes); os.write(branchArgumentBytes); + os.write(separatorBytes); + os.write(notifyCommitApiTokenBytes); } assertThat(connectionPOST.getResponseCode(), is(HttpURLConnection.HTTP_OK)); assertThat(connectionPOST.getResponseMessage(), is("OK")); @@ -231,7 +245,7 @@ public void testGETValidPathNoArgument() throws Exception { @Test public void testGETValidPathMandatoryArgument() throws Exception { - URL getURL = new URL(notifyCommitURL + "?" + urlArgument); + URL getURL = new URL(notifyCommitURL + "?" + urlArgument + separator + notifyCommitApiToken); HttpURLConnection connectionGET = (HttpURLConnection) getURL.openConnection(); connectionGET.setRequestMethod("GET"); connectionGET.connect(); @@ -242,7 +256,7 @@ public void testGETValidPathMandatoryArgument() throws Exception { @Test public void testGETValidPathMandatoryAndOptionalArgument() throws Exception { - URL getURL = new URL(notifyCommitURL + "?" + urlArgument + separator + branchArgument); + URL getURL = new URL(notifyCommitURL + "?" + urlArgument + separator + branchArgument + separator + notifyCommitApiToken); HttpURLConnection connectionGET = (HttpURLConnection) getURL.openConnection(); connectionGET.setRequestMethod("GET"); connectionGET.connect();
src/test/java/hudson/plugins/git/GitStatusTest.java+118 −21 modified@@ -54,6 +54,7 @@ public class GitStatusTest extends AbstractGitProject { private String repoURL; private String branch; private String sha1; + private String notifyCommitApiToken; @Before public void setUp() throws Exception { @@ -65,6 +66,9 @@ public void setUp() throws Exception { this.repoURL = new File(".").getAbsolutePath(); this.branch = "**"; this.sha1 = "7bb68ef21dc90bd4f7b08eca876203b2e049198d"; + if (jenkins.jenkins != null) { + this.notifyCommitApiToken = ApiTokenPropertyConfiguration.get().generateApiToken("test").getString("value"); + } } @After @@ -154,7 +158,7 @@ public void testDoNotifyCommitWithNoBranches() throws Exception { SCMTrigger bMasterTrigger = setupProjectWithTrigger("b", "master", false); SCMTrigger bTopicTrigger = setupProjectWithTrigger("b", "topic", false); - this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "", null, notifyCommitApiToken); Mockito.verify(aMasterTrigger).run(); Mockito.verify(aTopicTrigger).run(); Mockito.verify(bMasterTrigger, Mockito.never()).run(); @@ -170,7 +174,7 @@ public void testDoNotifyCommitWithNoMatchingUrl() throws Exception { SCMTrigger bMasterTrigger = setupProjectWithTrigger("b", "master", false); SCMTrigger bTopicTrigger = setupProjectWithTrigger("b", "topic", false); - this.gitStatus.doNotifyCommit(requestWithNoParameter, "nonexistent", "", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "nonexistent", "", null, notifyCommitApiToken); Mockito.verify(aMasterTrigger, Mockito.never()).run(); Mockito.verify(aTopicTrigger, Mockito.never()).run(); Mockito.verify(bMasterTrigger, Mockito.never()).run(); @@ -186,7 +190,7 @@ public void testDoNotifyCommitWithOneBranch() throws Exception { SCMTrigger bMasterTrigger = setupProjectWithTrigger("b", "master", false); SCMTrigger bTopicTrigger = setupProjectWithTrigger("b", "topic", false); - this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", null, notifyCommitApiToken); Mockito.verify(aMasterTrigger).run(); Mockito.verify(aTopicTrigger, Mockito.never()).run(); Mockito.verify(bMasterTrigger, Mockito.never()).run(); @@ -204,7 +208,7 @@ public void testDoNotifyCommitWithTwoBranches() throws Exception { SCMTrigger bTopicTrigger = setupProjectWithTrigger("b", "topic", false); SCMTrigger bFeatureTrigger = setupProjectWithTrigger("b", "feature/def", false); - this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master,topic,feature/def", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master,topic,feature/def", null, notifyCommitApiToken); Mockito.verify(aMasterTrigger).run(); Mockito.verify(aTopicTrigger).run(); Mockito.verify(aFeatureTrigger).run(); @@ -223,7 +227,7 @@ public void testDoNotifyCommitWithNoMatchingBranches() throws Exception { SCMTrigger bMasterTrigger = setupProjectWithTrigger("b", "master", false); SCMTrigger bTopicTrigger = setupProjectWithTrigger("b", "topic", false); - this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "nonexistent", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "nonexistent", null, notifyCommitApiToken); Mockito.verify(aMasterTrigger, Mockito.never()).run(); Mockito.verify(aTopicTrigger, Mockito.never()).run(); Mockito.verify(bMasterTrigger, Mockito.never()).run(); @@ -239,7 +243,7 @@ public void testDoNotifyCommitWithSlashesInBranchNames() throws Exception { SCMTrigger aSlashesTrigger = setupProjectWithTrigger("a", "name/with/slashes", false); - this.gitStatus.doNotifyCommit(requestWithParameter, "a", "name/with/slashes", null); + this.gitStatus.doNotifyCommit(requestWithParameter, "a", "name/with/slashes", null, notifyCommitApiToken); Mockito.verify(aSlashesTrigger).run(); Mockito.verify(bMasterTrigger, Mockito.never()).run(); @@ -252,7 +256,7 @@ public void testDoNotifyCommitWithParametrizedBranch() throws Exception { SCMTrigger bMasterTrigger = setupProjectWithTrigger("b", "master", false); SCMTrigger bTopicTrigger = setupProjectWithTrigger("b", "topic", false); - this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", null, notifyCommitApiToken); Mockito.verify(aMasterTrigger).run(); Mockito.verify(bMasterTrigger, Mockito.never()).run(); Mockito.verify(bTopicTrigger, Mockito.never()).run(); @@ -264,7 +268,7 @@ public void testDoNotifyCommitWithParametrizedBranch() throws Exception { public void testDoNotifyCommitWithIgnoredRepository() throws Exception { SCMTrigger aMasterTrigger = setupProjectWithTrigger("a", "master", true); - this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", null, ""); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", null, "", notifyCommitApiToken); Mockito.verify(aMasterTrigger, Mockito.never()).run(); assertEquals("URL: a SHA1: ", this.gitStatus.toString()); @@ -273,7 +277,7 @@ public void testDoNotifyCommitWithIgnoredRepository() throws Exception { @Test public void testDoNotifyCommitWithNoScmTrigger() throws Exception { setupProject("a", "master", null); - this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", null, ""); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", null, "", notifyCommitApiToken); // no expectation here, however we shouldn't have a build triggered, and no exception assertEquals("URL: a SHA1: ", this.gitStatus.toString()); @@ -321,7 +325,7 @@ private void doNotifyCommitWithTwoBranchesAndAdditionalParameter(final boolean a parameterMap.put("paramKey1", new String[] {"paramValue1"}); when(requestWithParameter.getParameterMap()).thenReturn(parameterMap); - this.gitStatus.doNotifyCommit(requestWithParameter, "a", "master,topic", null); + this.gitStatus.doNotifyCommit(requestWithParameter, "a", "master,topic", null, notifyCommitApiToken); Mockito.verify(aMasterTrigger).run(); Mockito.verify(aTopicTrigger).run(); Mockito.verify(bMasterTrigger, Mockito.never()).run(); @@ -345,39 +349,39 @@ private void doNotifyCommitWithTwoBranchesAndAdditionalParameter(final boolean a @Theory public void testDoNotifyCommitBranchWithSlash(@FromDataPoints("branchSpecPrefixes") String branchSpecPrefix) throws Exception { SCMTrigger trigger = setupProjectWithTrigger("remote", branchSpecPrefix + "feature/awesome-feature", false); - this.gitStatus.doNotifyCommit(requestWithNoParameter, "remote", "feature/awesome-feature", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "remote", "feature/awesome-feature", null, notifyCommitApiToken); Mockito.verify(trigger).run(); } @Theory public void testDoNotifyCommitBranchWithoutSlash(@FromDataPoints("branchSpecPrefixes") String branchSpecPrefix) throws Exception { SCMTrigger trigger = setupProjectWithTrigger("remote", branchSpecPrefix + "awesome-feature", false); - this.gitStatus.doNotifyCommit(requestWithNoParameter, "remote", "awesome-feature", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "remote", "awesome-feature", null, notifyCommitApiToken); Mockito.verify(trigger).run(); } @Theory public void testDoNotifyCommitBranchByBranchRef(@FromDataPoints("branchSpecPrefixes") String branchSpecPrefix) throws Exception { SCMTrigger trigger = setupProjectWithTrigger("remote", branchSpecPrefix + "awesome-feature", false); - this.gitStatus.doNotifyCommit(requestWithNoParameter, "remote", "refs/heads/awesome-feature", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "remote", "refs/heads/awesome-feature", null, notifyCommitApiToken); Mockito.verify(trigger).run(); } @Test public void testDoNotifyCommitBranchWithRegex() throws Exception { SCMTrigger trigger = setupProjectWithTrigger("remote", ":[^/]*/awesome-feature", false); - this.gitStatus.doNotifyCommit(requestWithNoParameter, "remote", "feature/awesome-feature", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "remote", "feature/awesome-feature", null, notifyCommitApiToken); Mockito.verify(trigger).run(); } @Test public void testDoNotifyCommitBranchWithWildcard() throws Exception { SCMTrigger trigger = setupProjectWithTrigger("remote", "origin/feature/*", false); - this.gitStatus.doNotifyCommit(requestWithNoParameter, "remote", "feature/awesome-feature", null); + this.gitStatus.doNotifyCommit(requestWithNoParameter, "remote", "feature/awesome-feature", null, notifyCommitApiToken); Mockito.verify(trigger).run(); } @@ -488,7 +492,7 @@ private Map<String, String[]> setupParameterMap(String extraValue) { @Test public void testDoNotifyCommit() throws Exception { /* No parameters */ setupNotifyProject(); - this.gitStatus.doNotifyCommit(requestWithNoParameter, repoURL, branch, sha1); + this.gitStatus.doNotifyCommit(requestWithNoParameter, repoURL, branch, sha1, notifyCommitApiToken); assertEquals("URL: " + repoURL + " SHA1: " + sha1 + " Branches: " + branch, this.gitStatus.toString()); @@ -529,7 +533,7 @@ private void doNotifyCommitWithExtraParameterAllowed(final boolean allowed, Stri setupNotifyProject(); String extraValue = "An-extra-value"; when(requestWithParameter.getParameterMap()).thenReturn(setupParameterMap(extraValue)); - this.gitStatus.doNotifyCommit(requestWithParameter, repoURL, branch, sha1); + this.gitStatus.doNotifyCommit(requestWithParameter, repoURL, branch, sha1, notifyCommitApiToken); String expected = "URL: " + repoURL + " SHA1: " + sha1 @@ -543,7 +547,7 @@ private void doNotifyCommitWithExtraParameterAllowed(final boolean allowed, Stri public void testDoNotifyCommitWithNullValueExtraParameter() throws Exception { setupNotifyProject(); when(requestWithParameter.getParameterMap()).thenReturn(setupParameterMap(null)); - this.gitStatus.doNotifyCommit(requestWithParameter, repoURL, branch, sha1); + this.gitStatus.doNotifyCommit(requestWithParameter, repoURL, branch, sha1, notifyCommitApiToken); assertEquals("URL: " + repoURL + " SHA1: " + sha1 + " Branches: " + branch, this.gitStatus.toString()); @@ -610,7 +614,7 @@ private void doNotifyCommitWithDefaultParameter(final boolean allowed, String sa String extraValue = "An-extra-value"; when(requestWithParameter.getParameterMap()).thenReturn(setupParameterMap(extraValue)); - this.gitStatus.doNotifyCommit(requestWithParameter, repoURL, branch, sha1); + this.gitStatus.doNotifyCommit(requestWithParameter, repoURL, branch, sha1, notifyCommitApiToken); String expected = "URL: " + repoURL + " SHA1: " + sha1 @@ -650,7 +654,7 @@ public void testDoNotifyCommitTriggeredHeadersLimited() throws Exception { projectTriggers[i] = setupProjectWithTrigger("a", "master", false); } - HttpResponse rsp = this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", null); + HttpResponse rsp = this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", null, notifyCommitApiToken); // Up to 10 "Triggered" headers + 1 extra warning are returned. StaplerRequest sReq = mock(StaplerRequest.class); @@ -674,11 +678,104 @@ public void testDoNotifyCommitWithWrongSha1Content() throws Exception { String content = "<img src=onerror=alert(1)>"; - HttpResponse rsp = this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", content); + HttpResponse rsp = this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", content, notifyCommitApiToken); HttpResponses.HttpResponseException responseException = ((HttpResponses.HttpResponseException) rsp); assertEquals(IllegalArgumentException.class, responseException.getCause().getClass()); assertEquals("Illegal SHA1", responseException.getCause().getMessage()); } + + @Test + @Issue("SECURITY-284") + public void testDoNotifyCommitWithValidSha1AndValidApiToken() throws Exception { + // when sha1 is provided build is scheduled right away instead of repo polling, so we do not check for trigger + FreeStyleProject project = setupNotifyProject(); + + this.gitStatus.doNotifyCommit(requestWithParameter, repoURL, branch, sha1, notifyCommitApiToken); + + jenkins.waitUntilNoActivity(); + FreeStyleBuild lastBuild = project.getLastBuild(); + + assertNotNull(lastBuild); + assertEquals(lastBuild.getNumber(), 1); + } + + @Test + @Issue("SECURITY-284") + public void testDoNotifyCommitWithInvalidApiToken() throws Exception { + setupProjectWithTrigger("a", "master", false); + StaplerResponse res = mock(StaplerResponse.class); + + HttpResponse httpResponse = this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", null, "invalid"); + httpResponse.generateResponse(null, res, null); + + Mockito.verify(res).sendError(403, "Invalid access token"); + } + + @Test + @Issue("SECURITY-284") + public void testDoNotifyCommitWithUnauthenticatedPollingAllowed() throws Exception { + GitStatus.NOTIFY_COMMIT_ACCESS_CONTROL = "disabled-for-polling"; + SCMTrigger trigger = setupProjectWithTrigger("a", "master", false); + + this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", null, null); + + Mockito.verify(trigger).run(); + } + + @Test + @Issue("SECURITY-284") + public void testDoNotifyCommitWithAllowModeRandomValue() throws Exception { + GitStatus.NOTIFY_COMMIT_ACCESS_CONTROL = "random"; + setupProjectWithTrigger("a", "master", false); + StaplerResponse res = mock(StaplerResponse.class); + + HttpResponse httpResponse = this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", null, null); + httpResponse.generateResponse(null, res, null); + + Mockito.verify(res).sendError(401, "An access token is required. Please refer to Git plugin documentation for details."); + } + + @Test + @Issue("SECURITY-284") + public void testDoNotifyCommitWithSha1AndAllowModePoll() throws Exception { + GitStatus.NOTIFY_COMMIT_ACCESS_CONTROL = "disabled-for-polling"; + setupProjectWithTrigger("a", "master", false); + StaplerResponse res = mock(StaplerResponse.class); + + HttpResponse httpResponse = this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", sha1, null); + httpResponse.generateResponse(null, res, null); + + Mockito.verify(res).sendError(401, "An access token is required when using the sha1 parameter. Please refer to Git plugin documentation for details."); + } + + @Test + @Issue("SECURITY-284") + public void testDoNotifyCommitWithSha1AndAllowModePollWithInvalidToken() throws Exception { + GitStatus.NOTIFY_COMMIT_ACCESS_CONTROL = "disabled-for-polling"; + setupProjectWithTrigger("a", "master", false); + StaplerResponse res = mock(StaplerResponse.class); + + HttpResponse httpResponse = this.gitStatus.doNotifyCommit(requestWithNoParameter, "a", "master", sha1, "invalid"); + httpResponse.generateResponse(null, res, null); + + Mockito.verify(res).sendError(403, "Invalid access token"); + } + + @Test + @Issue("SECURITY-284") + public void testDoNotifyCommitWithAllowModeSha1() throws Exception { + GitStatus.NOTIFY_COMMIT_ACCESS_CONTROL = "disabled"; + // when sha1 is provided build is scheduled right away instead of repo polling, so we do not check for trigger + FreeStyleProject project = setupNotifyProject(); + + this.gitStatus.doNotifyCommit(requestWithParameter, repoURL, branch, sha1, null); + + jenkins.waitUntilNoActivity(); + FreeStyleBuild lastBuild = project.getLastBuild(); + + assertNotNull(lastBuild); + assertEquals(lastBuild.getNumber(), 1); + } }
src/test/java/hudson/plugins/git/security/ApiTokenPropertyConfigurationTest.java+122 −0 added@@ -0,0 +1,122 @@ +package hudson.plugins.git.security; + +import com.gargoylesoftware.htmlunit.HttpMethod; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.WebResponse; +import com.gargoylesoftware.htmlunit.util.NameValuePair; +import hudson.plugins.git.ApiTokenPropertyConfiguration; +import jenkins.model.Jenkins; +import net.sf.json.JSONObject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; + +import java.util.Collection; +import java.util.Collections; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class ApiTokenPropertyConfigurationTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Before + public void init() { + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + MockAuthorizationStrategy authorizationStrategy = new MockAuthorizationStrategy(); + authorizationStrategy.grant(Jenkins.ADMINISTER).everywhere().to("alice"); + authorizationStrategy.grant(Jenkins.READ).everywhere().to("bob"); + j.jenkins.setAuthorizationStrategy(authorizationStrategy); + } + + @Test + public void testAdminPermissionRequiredToGenerateNewApiTokens() throws Exception { + try (JenkinsRule.WebClient wc = j.createWebClient()) { + wc.login("bob"); + WebRequest req = new WebRequest( + wc.createCrumbedUrl(ApiTokenPropertyConfiguration.get().getDescriptorUrl() + "/generate"), HttpMethod.POST); + req.setRequestBody("{\"apiTokenName\":\"test\"}"); + wc.setThrowExceptionOnFailingStatusCode(false); + + WebResponse res = wc.getPage(req).getWebResponse(); + + assertEquals(403, res.getStatusCode()); + assertTrue(res.getContentAsString().contains("bob is missing the Overall/Administer permission")); + } + } + + @Test + public void adminPermissionsRequiredToRevokeApiTokens() throws Exception { + try (JenkinsRule.WebClient wc = j.createWebClient()) { + wc.login("bob"); + WebRequest req = new WebRequest(wc.createCrumbedUrl(ApiTokenPropertyConfiguration.get().getDescriptorUrl() + "/revoke"), HttpMethod.POST); + wc.setThrowExceptionOnFailingStatusCode(false); + + WebResponse res = wc.getPage(req).getWebResponse(); + + assertEquals(403, res.getStatusCode()); + assertTrue(res.getContentAsString().contains("bob is missing the Overall/Administer permission")); + } + } + + @Test + public void testBasicGenerationAndRevocation() throws Exception { + try (JenkinsRule.WebClient wc = j.createWebClient()) { + wc.login("alice"); + WebRequest generateReq = new WebRequest( + wc.createCrumbedUrl(ApiTokenPropertyConfiguration.get().getDescriptorUrl() + "/generate"), HttpMethod.POST); + generateReq.setRequestParameters(Collections.singletonList(new NameValuePair("apiTokenName", "token"))); + String uuid = JSONObject.fromObject(wc.getPage(generateReq).getWebResponse().getContentAsString()).getJSONObject("data").getString("uuid"); + + generateReq.setRequestParameters(Collections.singletonList(new NameValuePair("apiTokenName", "nekot"))); + String uuid2 = JSONObject.fromObject(wc.getPage(generateReq).getWebResponse().getContentAsString()).getJSONObject("data").getString("uuid"); + + Collection<ApiTokenPropertyConfiguration.HashedApiToken> apiTokens = ApiTokenPropertyConfiguration.get().getApiTokens(); + assertThat(apiTokens, allOf( + iterableWithSize(2), + hasItem( + allOf( + hasProperty("name", is("token")), + hasProperty("uuid", is(uuid)) + ) + ), + hasItem( + allOf( + hasProperty("name", is("nekot")), + hasProperty("uuid", is(uuid2)) + ) + ) + )); + + WebRequest revokeReq = new WebRequest( + wc.createCrumbedUrl(ApiTokenPropertyConfiguration.get().getDescriptorUrl() + "/revoke"), HttpMethod.POST); + revokeReq.setRequestParameters(Collections.singletonList(new NameValuePair("apiTokenUuid", uuid))); + wc.getPage(revokeReq); + + apiTokens = ApiTokenPropertyConfiguration.get().getApiTokens(); + assertThat(apiTokens, allOf( + iterableWithSize(1), + hasItem( + allOf( + hasProperty("name", is("nekot")), + hasProperty("uuid", is(uuid2)) + ) + ) + )); + } + } + + @Test + public void isValidApiTokenReturnsTrueIfGivenApiTokenExists() { + JSONObject json = ApiTokenPropertyConfiguration.get().generateApiToken("test"); + + assertTrue(ApiTokenPropertyConfiguration.get().isValidApiToken(json.getString("value"))); + } + +}
src/test/java/jenkins/plugins/git/GitSampleRepoRule.java+4 −1 modified@@ -29,6 +29,7 @@ import hudson.Launcher; import hudson.model.TaskListener; import hudson.plugins.git.GitSCM; +import hudson.plugins.git.ApiTokenPropertyConfiguration; import hudson.util.StreamTaskListener; import java.io.ByteArrayOutputStream; import java.io.File; @@ -88,7 +89,9 @@ public final boolean mkdirs(String rel) throws IOException { public void notifyCommit(JenkinsRule r) throws Exception { synchronousPolling(r); - WebResponse webResponse = r.createWebClient().goTo("git/notifyCommit?url=" + bareUrl(), "text/plain").getWebResponse(); + String notifyCommitToken = ApiTokenPropertyConfiguration.get().generateApiToken("notifyCommit").getString("value"); + WebResponse webResponse = r.createWebClient() + .goTo("git/notifyCommit?url=" + bareUrl() + "&token=" + notifyCommitToken, "text/plain").getWebResponse(); LOGGER.log(Level.FINE, webResponse.getContentAsString()); for (NameValuePair pair : webResponse.getResponseHeaders()) { if (pair.getName().equals("Triggered")) {
src/test/java/jenkins/plugins/git/GitSCMSourceTest.java+3 −1 modified@@ -11,6 +11,7 @@ import hudson.FilePath; import hudson.plugins.git.GitStatus; import hudson.plugins.git.GitTool; +import hudson.plugins.git.ApiTokenPropertyConfiguration; import hudson.scm.SCMDescriptor; import hudson.tools.CommandInstaller; import hudson.tools.InstallSourceProperty; @@ -92,11 +93,12 @@ public void setup() { @Test @Deprecated public void testSourceOwnerTriggeredByDoNotifyCommit() throws Exception { + String notifyCommitApiToken = ApiTokenPropertyConfiguration.get().generateApiToken("test").getString("value"); GitSCMSource gitSCMSource = new GitSCMSource("id", REMOTE, "", "*", "", false); GitSCMSourceOwner scmSourceOwner = setupGitSCMSourceOwner(gitSCMSource); jenkins.getInstance().add(scmSourceOwner, "gitSourceOwner"); - gitStatus.doNotifyCommit(mock(HttpServletRequest.class), REMOTE, "master", ""); + gitStatus.doNotifyCommit(mock(HttpServletRequest.class), REMOTE, "master", "", notifyCommitApiToken); SCMHeadEvent event = jenkins.getInstance().getExtensionList(SCMEventListener.class).get(SCMEventListenerImpl.class)
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-8xwj-2wgh-gprhghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-36882ghsaADVISORY
- www.openwall.com/lists/oss-security/2022/07/27/1ghsamailing-listx_refsource_MLISTWEB
- github.com/jenkinsci/git-plugin/commit/b46165c74a0bf15e08763de2e506005624d5d238ghsaWEB
- www.jenkins.io/security/advisory/2022-07-27/ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.