CVE-2022-45385
Description
Missing permission check in Jenkins CloudBees Docker Hub/Registry Notification Plugin allows unauthenticated attackers to trigger builds for arbitrary repositories.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Missing permission check in Jenkins CloudBees Docker Hub/Registry Notification Plugin allows unauthenticated attackers to trigger builds for arbitrary repositories.
CloudBees Docker Hub/Registry Notification Plugin versions 2.6.2 and earlier contain a missing permission check [1]. This flaw allows unauthenticated attackers to trigger builds of jobs corresponding to an attacker-specified repository [2][3].
The attack is straightforward: an attacker can send a specially crafted HTTP POST request to the plugin's notification endpoint without providing any credentials [1]. The plugin incorrectly processes the request without verifying that the user has the Item/Build permission, enabling arbitrary repository names to be specified [2].
By exploiting this vulnerability, an attacker can cause Jenkins to build jobs that are configured to respond to Docker Hub or registry notifications [3]. This can lead to unauthorized execution of build steps, potentially using attacker-controlled parameters, which may result in further compromise of the Jenkins environment or connected systems [1].
The issue is fixed in CloudBees Docker Hub/Registry Notification Plugin version 2.6.2.1 [4]. Users are advised to update immediately. No workaround is available [1].
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:dockerhub-notificationMaven | < 2.6.2.1 | 2.6.2.1 |
Affected products
2- Range: unspecified
Patches
11163d4f297afSECURITY-2843
10 files changed · +496 −6
src/main/java/org/jenkinsci/plugins/registry/notification/token/ApiTokens.java+215 −0 added@@ -0,0 +1,215 @@ +/** + * The MIT License + * + * Copyright (c) 2022, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.jenkinsci.plugins.registry.notification.token; + +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.verb.POST; + +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.Date; +import java.util.List; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Extension +@Restricted(NoExternalUse.class) +@Symbol("dockerHubApiTokens") +public class ApiTokens extends GlobalConfiguration implements PersistentDescriptor { + + private static final Logger LOGGER = Logger.getLogger(ApiTokens.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 ApiTokens() { + this.apiTokens = new ArrayList<>(); + } + + @NonNull + @Override + public GlobalConfigurationCategory getCategory() { + return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class); + } + + public static ApiTokens get() { + return GlobalConfiguration.all().get(ApiTokens.class); + } + + @POST + public HttpResponse doGenerate(StaplerRequest req) { + // Require admin privileges to change the API tokens + 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); + } + + @POST + public HttpResponse doRevoke(StaplerRequest req) { + // Require admin privileges to change the API tokens + 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 final Date created; + + private HashedApiToken(String name, String hash) { + this.uuid = UUID.randomUUID().toString(); + this.name = name; + this.hash = hash; + this.created = new Date(); + } + + private HashedApiToken(String uuid, String name, String hash, final Date created) { + this.uuid = uuid; + this.name = name; + this.hash = hash; + this.created = created; + } + + public String getUuid() { + return uuid; + } + + public String getName() { + return name; + } + + public String getHash() { + return hash; + } + + public Date getCreated() { + return new Date(created.getTime()); + } + + private boolean match(byte[] hashedBytes) { + byte[] hashFromHex; + try { + hashFromHex = Util.fromHexString(hash); + } catch (NumberFormatException e) { + LOGGER.log(Level.WARNING, "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/org/jenkinsci/plugins/registry/notification/webhook/JSONWebHook.java+45 −1 modified@@ -34,20 +34,25 @@ import org.jenkinsci.plugins.registry.notification.Coordinator; import org.jenkinsci.plugins.registry.notification.DockerHubTrigger; import org.jenkinsci.plugins.registry.notification.TriggerStore; +import org.jenkinsci.plugins.registry.notification.token.ApiTokens; +import org.kohsuke.stapler.Ancestor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.interceptor.RequirePOST; import org.kohsuke.stapler.interceptor.RespondSuccess; +import org.springframework.security.access.AccessDeniedException; import java.io.IOException; +import java.io.Serializable; import java.net.URLDecoder; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; public abstract class JSONWebHook implements UnprotectedRootAction { private static final Logger logger = Logger.getLogger(JSONWebHook.class.getName()); + private static /*almost final*/ boolean DO_NOT_REQUIRE_API_TOKEN = jenkins.util.SystemProperties.getBoolean(JSONWebHook.class.getName() + "DO_NOT_REQUIRE_API_TOKEN"); public String getIconFileName() { return null; @@ -60,7 +65,9 @@ public String getDisplayName() { @RequirePOST @RespondSuccess public void doNotify(@QueryParameter(required = false) String payload, StaplerRequest request, StaplerResponse response) throws IOException { - + if (!DO_NOT_REQUIRE_API_TOKEN) { + checkValidApiToken(request, response); + } WebHookPayload hookPayload = null; if (payload != null) { try { @@ -82,6 +89,23 @@ public void doNotify(@QueryParameter(required = false) String payload, StaplerRe } } + private void checkValidApiToken(final StaplerRequest request, final StaplerResponse response) throws IOException { + final Ancestor ancestor = request.findAncestor(ValidApiToken.class); + if (ancestor == null) { + response.sendError(403, "No valid API token provided."); + throw new AccessDeniedException("No valid API token provided."); + } + } + + public ValidApiToken getDynamic(String token, StaplerResponse rsp) throws IOException { + if (ApiTokens.get().isValidApiToken(token)) { + return new ValidApiToken(token, this); + } else { + rsp.sendError(403, "No valid API token provided."); + return null; + } + } + /** * Stapler entry for the multi build result page * @param sha the id of the trigger data. @@ -246,4 +270,24 @@ private List<ParameterValue> getDefaultParametersValues() { } } + + public static class ValidApiToken { + private final String token; + private final JSONWebHook delegate; + + public ValidApiToken(final String token, final JSONWebHook delegate) { + this.token = token; + this.delegate = delegate; + } + + public String getToken() { + return token; + } + + @RequirePOST + @RespondSuccess + public void doNotify(@QueryParameter(required = false) String payload, StaplerRequest request, StaplerResponse response) throws IOException { + delegate.doNotify(payload, request, response); + } + } }
src/main/resources/org/jenkinsci/plugins/registry/notification/DockerHubTrigger/help.groovy+1 −1 modified@@ -27,7 +27,7 @@ import jenkins.model.Jenkins def webHookUrl(String rootAction) { String rootUrl = Jenkins.instance?.getRootUrl() ?: "http://myJENKINS/"; - return rootUrl + "${rootAction}/notify" + return rootUrl + "${rootAction}/{api-token}/notify" } p(_("generalBlurb"))
src/main/resources/org/jenkinsci/plugins/registry/notification/token/ApiTokens/config.jelly+60 −0 added@@ -0,0 +1,60 @@ +<?jelly escape-by-default='true'?> + +<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form" xmlns:l="/lib/layout" xmlns:fmt="jelly:fmt"> + <f:section title="${%DockerHub/Registry web-hook tokens}"> + <st:adjunct includes="org.jenkinsci.plugins.registry.notification.token.ApiTokens.resources" /> + <f:entry title="${%Current access tokens}" help="${descriptor.getHelpFile('tokens')}"> + <div class="dockerhub-api-token-list"> + <j:set var="apiTokens" value="${instance.apiTokens}" /> + <div class="dockerhub-api-token-list-empty-item ${apiTokens == null || apiTokens.isEmpty() ? '' : 'hidden'}"> + <div class="dockerhub-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="dockerhub-api-token-uuid-input" name="apiTokenUuid" value="${apiToken.uuid}" /> + <div class="dockerhub-api-token-list-item-row dockerhub-api-token-list-existing-token"> + <span class="dockerhub-api-token-created"> + <fmt:formatDate value="${apiToken.created}" type="both" dateStyle="medium" timeStyle="medium"/> + </span> + <f:textbox readonly="true" value="${apiToken.name}" /> + <!-- onclick handler for that button is defined in resources.js --> + <a href="#" class="yui-button dockerhub-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="dockerhub-api-token-list-item"> + <div class="dockerhub-api-token-list-item-row"> + <input type="hidden" class="dockerhub-api-token-uuid-input" name="apiTokenUuid" value="${apiToken.uuid}" /> + <f:textbox clazz="dockerhub-api-token-name-input" name="apiTokenName" placeholder="${%Access token name}"/> + <span class="dockerhub-new-api-token-value hidden"><!-- to be filled by JS --></span> + <span class="yui-button dockerhub-api-token-save-button"> + <!-- onclick handler for that button is defined in resources.js --> + <button type="button" tabindex="0" data-target-url="${descriptor.descriptorFullUrl}/generate"> + ${%Generate} + </button> + </span> + <span class="dockerhub-api-token-cancel-button"> + <f:repeatableDeleteButton value="${%Cancel}" /> + </span> + <l:copyButton message="${%Copied}" text="" clazz="hidden" tooltip="${%Copy to clipboard}" /> + <!-- onclick handler for that button is defined in resources.js --> + <a href="#" class="yui-button dockerhub-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 dockerhub-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/org/jenkinsci/plugins/registry/notification/token/ApiTokens/help-tokens.html+13 −0 added@@ -0,0 +1,13 @@ +<div xmlns="http://www.w3.org/1999/html"> + <p> + These access tokens serve as a way of authenticating requests to the + <code>dockerhub-webhook</code>, <code>dockerregistry-webhook</code> and <code>acr-webhook</code> endpoints. + </p> + <p> + By default, all requests to those endpoints must include a valid token in the path component before <code>/notify</code>. + E.g. <code>https://jenkins/dockerhub-webhook/{token}/notify</code> + 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>org.jenkinsci.plugins.registry.notification.webhook.JSONWebHook.DO_NOT_REQUIRE_API_TOKEN=true</code></pre> + </p> +</div>
src/main/resources/org/jenkinsci/plugins/registry/notification/token/ApiTokens/resources.css+21 −0 added@@ -0,0 +1,21 @@ +.dockerhub-api-token-list .dockerhub-api-token-list-item-row { + display: flex; + align-items: center; + max-width: 700px; +} +.dockerhub-api-token-list .dockerhub-api-token-list-item-row.dockerhub-api-token-list-existing-api-token { + justify-content: space-between; +} +.dockerhub-api-token-list .dockerhub-api-token-list-item .hidden, .dockerhub-api-token-list .dockerhub-api-token-list-empty-item.hidden { + display: none; +} + +.dockerhub-api-token-list .dockerhub-api-token-revoke-button, .dockerhub-api-token-list .dockerhub-new-api-token-value { + padding: 0 0.5rem; +} +.dockerhub-api-token-list .dockerhub-api-token-warning-message, .dockerhub-api-token-list .dockerhub-api-token-save-button { + margin: 0.5rem 0; +} +.dockerhub-api-token-created { + font-size: smaller; +}
src/main/resources/org/jenkinsci/plugins/registry/notification/token/ApiTokens/resources.js+107 −0 added@@ -0,0 +1,107 @@ +/** +* Registering the onclick handler on all the "Revoke" buttons for API Token. +*/ +Behaviour.specify(".dockerhub-api-token-revoke-button", 'ApiTokens', 0, function(button) { + // DEV MEMO: + // While un-inlining the onclick handler, we are trying to avoid modifying the existing source code and functions. + // In order to keep consistency with the existing code, we share the api-token-revoke-button with the revokeApiToken method + // which is then navigating the DOM in order to retrieve a the token to revoke. + // While this could be done setting additional data on the button itself and retrieving it without DOM navigation, + // this would need to be done in another contribution. + button.onclick = (_) => revokeDockerHubApiToken(button); +}) + +function revokeDockerHubApiToken(anchorRevoke) { + const repeatedChunk = anchorRevoke.up('.repeated-chunk'); + const apiTokenList = repeatedChunk.up('.dockerhub-api-token-list'); + const confirmMessage = anchorRevoke.getAttribute('data-confirm'); + const targetUrl = anchorRevoke.getAttribute('data-target-url'); + const inputUuid = repeatedChunk.querySelector('.dockerhub-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; +} + +/** +* Registering the onclick handler on all the "Generate" buttons for API Token. +*/ +Behaviour.specify(".dockerhub-api-token-save-button", 'ApiTokens', 0, function(buttonContainer) { + // DEV MEMO: + // While un-inlining the onclick handler, we are trying to avoid modifying the existing source code and functions. + // In order to keep consistency with the existing code, we add our onclick handler on the button element which is contained in the + // api-token-save-button that we identify. While this could be refactored to directly identify the button, this would need to be done in an other + // contribution. + const button = buttonContainer.getElementsByTagName('button')[0]; + button.onclick = (_) => saveDockerHubApiToken(button); +}) + +function saveDockerHubApiToken(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('.dockerhub-api-token-list'); + const nameInput = repeatedChunk.querySelector('.dockerhub-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('.dockerhub-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('.dockerhub-api-token-uuid-input'); + uuidInput.value = uuid; + + const warningMessage = repeatedChunk.querySelector('.dockerhub-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('.dockerhub-api-token-revoke-button'); + revokeButton.removeClassName('hidden'); + + const cancelButton = repeatedChunk.querySelector('.dockerhub-api-token-cancel-button'); + cancelButton.addClassName('hidden'); + + repeatedChunk.addClassName('dockerhub-api-token-list-fresh-item'); + + adjustEmptyListMessage(apiTokenList); + } + }); +} + +function adjustEmptyListMessage(apiTokenList) { + const emptyListMessageClassList = apiTokenList.querySelector('.dockerhub-api-token-list-empty-item').classList; + + const apiTokenListLength = apiTokenList.querySelectorAll('.dockerhub-api-token-list-existing-item, .dockerhub-api-token-list-fresh-item').length; + if (apiTokenListLength >= 1) { + emptyListMessageClassList.add("hidden"); + } else { + emptyListMessageClassList.remove("hidden"); + } +}
src/test/java/org/jenkinsci/plugins/registry/notification/ACRWebHookTest.java+11 −1 modified@@ -9,10 +9,12 @@ import org.apache.commons.io.IOUtils; import org.jenkinsci.plugins.registry.notification.opt.TriggerOption; import org.jenkinsci.plugins.registry.notification.opt.impl.TriggerOnSpecifiedImageNames; +import org.jenkinsci.plugins.registry.notification.token.ApiTokens; import org.jenkinsci.plugins.registry.notification.webhook.Http; import org.jenkinsci.plugins.registry.notification.webhook.acr.ACRPushNotification; import org.jenkinsci.plugins.registry.notification.webhook.acr.ACRWebHook; import org.jenkinsci.plugins.registry.notification.webhook.acr.ACRWebHookCause; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -34,8 +36,16 @@ public class ACRWebHookTest { @Rule public JenkinsRule j = new JenkinsRule(); + private String token; + private String getWebHookURL() throws IOException { - return this.j.getURL() + ACRWebHook.URL_NAME + "/notify"; + return this.j.getURL() + ACRWebHook.URL_NAME + "/" + token + "/notify"; + } + + @Before + public void setUp() { + final JSONObject test = ApiTokens.get().generateApiToken("test"); + token = test.getString("value"); } @Test
src/test/java/org/jenkinsci/plugins/registry/notification/CoordinatorTest.java+12 −2 modified@@ -31,8 +31,10 @@ import net.sf.json.JSONObject; import org.apache.commons.io.IOUtils; import org.jenkinsci.plugins.registry.notification.opt.impl.TriggerOnSpecifiedImageNames; +import org.jenkinsci.plugins.registry.notification.token.ApiTokens; import org.jenkinsci.plugins.registry.notification.webhook.Http; import org.jenkinsci.plugins.registry.notification.webhook.dockerhub.DockerHubWebHook; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -68,6 +70,14 @@ public class CoordinatorTest { public JenkinsRule j = new JenkinsRule(); private static final Response resp = new Response(); + private String token; + + @Before + public void setUp() { + final JSONObject test = ApiTokens.get().generateApiToken("test"); + token = test.getString("value"); + } + @Test public void testTwoTriggered() throws Exception { FreeStyleProject one = j.createFreeStyleProject(); @@ -83,7 +93,7 @@ public void testTwoTriggered() throws Exception { JSONObject json = JSONObject.fromObject(IOUtils.toString(getClass().getResourceAsStream("/own-repository-payload.json"))); json.put("callback_url", j.getURL() + "fake-dockerhub/respond"); - String url = j.getURL() + DockerHubWebHook.URL_NAME + "/notify"; + String url = j.getURL() + DockerHubWebHook.URL_NAME + "/" + token + "/notify"; assertEquals(302, Http.post(url, json)); synchronized (resp) { resp.wait(); @@ -126,7 +136,7 @@ public void testOneTriggered() throws Exception { JSONObject json = JSONObject.fromObject(IOUtils.toString(getClass().getResourceAsStream("/own-repository-payload.json"))); json.put("callback_url", j.getURL() + "fake-dockerhub/respond"); - String url = j.getURL() + DockerHubWebHook.URL_NAME + "/notify"; + String url = j.getURL() + DockerHubWebHook.URL_NAME + "/" + token + "/notify"; assertEquals(302, Http.post(url, json)); synchronized (resp) { resp.wait();
src/test/java/org/jenkinsci/plugins/registry/notification/RegistryWebHookTest.java+11 −1 modified@@ -31,9 +31,11 @@ import net.sf.json.JSONObject; import org.apache.commons.io.IOUtils; import org.jenkinsci.plugins.registry.notification.opt.impl.TriggerOnSpecifiedImageNames; +import org.jenkinsci.plugins.registry.notification.token.ApiTokens; import org.jenkinsci.plugins.registry.notification.webhook.Http; import org.jenkinsci.plugins.registry.notification.webhook.WebHookCause; import org.jenkinsci.plugins.registry.notification.webhook.dockerregistry.DockerRegistryWebHook; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -57,6 +59,14 @@ public class RegistryWebHookTest { @Rule public JenkinsRule j = new JenkinsRule(); + private String token; + + @Before + public void setUp() { + final JSONObject test = ApiTokens.get().generateApiToken("test"); + token = test.getString("value"); + } + @Test public void testTwoTriggered() throws Exception { HashSet<String> repositories = new HashSet<String>() {{ @@ -122,7 +132,7 @@ private void simulatePushNotification(Integer expectedHits, String payloadResour pushNotificationRunListener.setExpectedCauses(repositories); assertThat(pushNotificationRunListener.getExpectedCauses(), hasSize(repositories.size())); JSONObject json = JSONObject.fromObject(IOUtils.toString(getClass().getResourceAsStream(payloadResource))); - String url = j.getURL() + DockerRegistryWebHook.URL_NAME + "/notify"; + String url = j.getURL() + DockerRegistryWebHook.URL_NAME + "/" + token + "/notify"; assertEquals(200, Http.post(url, json)); j.waitUntilNoActivity(); assertThat(pushNotificationRunListener.getExpectedCauses(), hasSize(0));
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-v535-pc6r-77qhghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-45385ghsaADVISORY
- www.openwall.com/lists/oss-security/2022/11/15/4ghsamailing-listWEB
- github.com/jenkinsci/dockerhub-notification-plugin/commit/1163d4f297af23266c032fc66bd603b97f9ecd4bghsaWEB
- www.jenkins.io/security/advisory/2022-11-15/ghsaWEB
News mentions
1- Jenkins Security Advisory 2022-11-15Jenkins Security Advisories · Nov 15, 2022