VYPR
Moderate severityNVD Advisory· Published Nov 15, 2022· Updated Apr 30, 2025

CVE-2022-45385

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.

PackageAffected versionsPatched versions
org.jenkins-ci.plugins:dockerhub-notificationMaven
< 2.6.2.12.6.2.1

Affected products

2

Patches

1
1163d4f297af

SECURITY-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

News mentions

1