CVE-2023-37953
Description
A missing permission check in Jenkins mabl Plugin 0.0.46 and earlier allows attackers with Overall/Read permission to connect to an attacker-specified URL using attacker-specified credentials IDs obtained through another method, capturing credentials stored in Jenkins.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Missing permission check in Jenkins mabl Plugin allows attackers with Overall/Read to capture credentials by connecting to attacker-controlled URL.
Vulnerability
Description
The mabl Plugin for Jenkins, versions 0.0.46 and earlier, contains a missing permission check that allows attackers with Overall/Read permission to connect to an attacker-specified URL using attacker-specified credential IDs obtained through another method [1][2][3]. This flaw exists in the plugin's implementation, which does not verify that the user has the necessary permissions to perform such connections.
Exploitation
An attacker must have Overall/Read permission on the Jenkins instance and must obtain credential IDs (e.g., via another vulnerability or information disclosure). The attacker can then configure the mabl Plugin to connect to a URL they control, using those credential IDs [1][4]. No additional authentication or privileges are required beyond Overall/Read.
Impact
Successful exploitation allows the attacker to capture credentials stored in Jenkins by tricking the plugin into sending them to an attacker-controlled server [1][2][3]. This could lead to further compromise of the Jenkins environment or other integrated systems.
Mitigation
The vulnerability is fixed in mabl Plugin version 0.0.47 [1][2][4]. Users should upgrade immediately. The fix also includes an increased minimum Jenkins version [4]. No workarounds are available.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
com.mabl.integration.jenkins:mabl-integrationMaven | < 0.0.47 | 0.0.47 |
Affected products
3- Range: <=0.0.46
- Range: 0
Patches
14e21f498b070[SECURITY-3127][SECURITY-3137] (#64)
8 files changed · +281 −148
pom.xml+1 −1 modified@@ -21,7 +21,7 @@ <properties> <!-- NOTE: this version drives the minimum Jenkins version that the plugin can be installed on --> - <jenkins.version>2.222.4</jenkins.version> + <jenkins.version>2.319.1</jenkins.version> <java.level>8</java.level> <no-test-jar>false</no-test-jar>
README.md+6 −2 modified@@ -20,7 +20,7 @@ This plugin allows easy launching of [mabl](https://www.mabl.com) tests as a ste * [Deployment](#deployment) ## Plugin Installation -Install the [plugin](https://plugins.jenkins.io/mabl-integration) into your Jenkins `v2.222+` server from the *Available Plugins* tab by searching for "mabl". +Install the [plugin](https://plugins.jenkins.io/mabl-integration) into your Jenkins `v2.319.1+` server from the *Available Plugins* tab by searching for "mabl". ### Features @@ -30,7 +30,7 @@ Install the [plugin](https://plugins.jenkins.io/mabl-integration) into your Jenk ### Requirements -- Minimum Jenkins version: *2.222.4* +- Minimum Jenkins version: *2.319.1* - Minimum Java runtime version: *8* - mabl API key - See [integration @@ -186,6 +186,10 @@ Note that ### Change Log +#### v0.0.47 (07/06/2023) +- Patched security vulnerabilities +- Increased minimum Jenkins version + #### v0.0.46 (11/15/2022) - Replace usages of deprecated imports
src/main/java/com/mabl/integration/jenkins/MablStepBuilder.java+156 −76 modified@@ -1,7 +1,13 @@ package com.mabl.integration.jenkins; +import com.cloudbees.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import hudson.model.Job; +import hudson.model.Queue; +import hudson.model.queue.Tasks; +import hudson.security.ACL; +import jenkins.model.Jenkins; import org.jenkinsci.plugins.plaincredentials.StringCredentials; import com.mabl.integration.jenkins.domain.GetApiKeyResult; import com.mabl.integration.jenkins.domain.GetApplicationsResult; @@ -16,7 +22,6 @@ import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; -import hudson.security.ACL; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; import hudson.util.FormValidation; @@ -31,26 +36,25 @@ import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.verb.POST; import javax.annotation.Nonnull; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.util.Collections; -import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; import java.util.logging.Logger; -import static com.cloudbees.plugins.credentials.CredentialsProvider.*; import static com.mabl.integration.jenkins.MablStepConstants.BUILD_STEP_DISPLAY_NAME; import static com.mabl.integration.jenkins.MablStepConstants.EXECUTION_STATUS_POLLING_INTERNAL_MILLISECONDS; import static com.mabl.integration.jenkins.MablStepConstants.EXECUTION_TIMEOUT_SECONDS; -import static com.mabl.integration.jenkins.MablStepConstants.DEFAULT_MABL_APP_BASE_URL; -import static com.mabl.integration.jenkins.MablStepConstants.DEFAULT_MABL_API_BASE_URL; +import static com.mabl.integration.jenkins.MablStepConstants.MABL_APP_BASE_URL; +import static com.mabl.integration.jenkins.MablStepConstants.MABL_API_BASE_URL; import static com.mabl.integration.jenkins.MablStepConstants.PLUGIN_SYMBOL; import static com.mabl.integration.jenkins.MablStepConstants.TEST_OUTPUT_XML_FILENAME; import static java.util.concurrent.TimeUnit.SECONDS; @@ -175,10 +179,22 @@ public void perform( ) throws InterruptedException { final PrintStream outputStream = listener.getLogger(); + + StringCredentials credentials = CredentialsProvider.findCredentialById( + restApiKeyId, + StringCredentials.class, + run, + Collections.emptyList() + ); + + if (credentials == null) { + throw new IllegalStateException("No credentials found for API key provided"); + } + final MablRestApiClient client = new MablRestApiClientImpl( - StringUtils.isBlank(apiBaseUrl) ? DEFAULT_MABL_API_BASE_URL : apiBaseUrl, - getRestApiSecret(getRestApiKeyId()), - StringUtils.isBlank(appBaseUrl) ? DEFAULT_MABL_APP_BASE_URL : appBaseUrl, + MABL_API_BASE_URL, + credentials.getSecret(), + MABL_APP_BASE_URL, disableSslVerification ); @@ -259,23 +275,6 @@ private EnvVars getEnvironmentVars(Run<?, ?> build, TaskListener listener) { return environmentVars; } - static Secret getRestApiSecret(final String restApiKeyId) { - if (restApiKeyId == null) { - return null; - } - - Secret secretKey = null; - List<StringCredentials> stringCredentials = - CredentialsProvider.lookupCredentials(StringCredentials.class, (Item) null, ACL.SYSTEM, Collections.emptyList()); - for (StringCredentials cred : stringCredentials) { - if (restApiKeyId.equals(cred.getId())) { - secretKey = cred.getSecret(); - break; - } - } - return secretKey; - } - /** * Descriptor used in views. Centralized metadata store for all {@link MablStepBuilder} instances. */ @@ -312,43 +311,67 @@ public String getDisplayName() { return BUILD_STEP_DISPLAY_NAME; } - + @POST public FormValidation doValidateForm( @QueryParameter("restApiKeyId") final String restApiKeyId, @QueryParameter("environmentId") final String environmentId, @QueryParameter("applicationId") final String applicationId, @QueryParameter("disableSslVerification") final boolean disableSslVerification, - @QueryParameter("apiBaseUrl") final String apiBaseUrl, - @QueryParameter("appBaseUrl") final String appBaseUrl + @AncestorInPath final Job job ) { - return MablStepBuilderValidator.validateForm( - restApiKeyId, environmentId, applicationId, disableSslVerification, apiBaseUrl, appBaseUrl); + if (job == null) { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + } else { + job.checkPermission(Item.CONFIGURE); + } + return MablStepBuilderValidator.validateForm(restApiKeyId, environmentId, applicationId, disableSslVerification, job); } public ListBoxModel doFillRestApiKeyIdItems( @AncestorInPath Item item, - @QueryParameter String restApiKeyId) { + @QueryParameter String restApiKeyId + ) { + Jenkins.get().checkPermission(Job.CONFIGURE); StandardListBoxModel result = new StandardListBoxModel(); - if (restApiKeyId != null) { - result.add(restApiKeyId); - } - List<StringCredentials> creds = - lookupCredentials(StringCredentials.class, item, ACL.SYSTEM, - Collections.emptyList()); - for (StringCredentials cred : creds) { - result.add(cred.getId()); + if (item == null) { + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + return result.includeCurrentValue(restApiKeyId); + } + } else { + if (!item.hasPermission(Item.EXTENDED_READ) + && !item.hasPermission(CredentialsProvider.USE_ITEM)) { + return result.includeCurrentValue(restApiKeyId); + } } - return result.includeEmptyValue(); + + return result + .includeEmptyValue() + .includeMatchingAs( + item instanceof Queue.Task ? Tasks.getAuthenticationOf((Queue.Task) item) : ACL.SYSTEM, + item, + StringCredentials.class, + Collections.emptyList(), + CredentialsMatchers.always() + ); } public FormValidation doCheckRestApiKeyIds( @AncestorInPath Item item, @QueryParameter String value ) { + if (item == null) { + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + return FormValidation.ok(); + } + } else { + if (!item.hasPermission(Item.EXTENDED_READ) + && !item.hasPermission(CredentialsProvider.USE_ITEM)) { + return FormValidation.warning("Insufficient permissions"); + } + } + if (StringUtils.isBlank(value)) { return FormValidation.warning("Provide a credentials ID"); - } else if (!item.hasPermission(Item.EXTENDED_READ) && !item.hasPermission(USE_ITEM)) { - return FormValidation.warning("Insufficient permissions"); } if (value.startsWith("${") && value.endsWith("}")) { @@ -358,27 +381,48 @@ public FormValidation doCheckRestApiKeyIds( return FormValidation.ok(); } + @POST public ListBoxModel doFillApplicationIdItems( @QueryParameter String restApiKeyId, @QueryParameter boolean disableSslVerification, - @QueryParameter String apiBaseUrl, - @QueryParameter String appBaseUrl + @AncestorInPath Item item ) { + Jenkins.get().checkPermission(Job.CONFIGURE); if (StringUtils.isBlank(restApiKeyId)) { return getSelectValidApiKeyListBoxModel(); } - Secret secretKey = getRestApiSecret(restApiKeyId); - if (secretKey != null) { - final MablRestApiClient client = - createMablRestApiClient( - secretKey, - disableSslVerification, - apiBaseUrl, - appBaseUrl - ); - return getApplicationIdItems(client); + + if (item == null) { + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + return new ListBoxModel(); + } + } else { + if (!item.hasPermission(Item.EXTENDED_READ) + && !item.hasPermission(CredentialsProvider.USE_ITEM)) { + return new ListBoxModel(); + } } - return new ListBoxModel(); + + StringCredentials credentials = CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentials( + StringCredentials.class, + item, + item instanceof Queue.Task ? Tasks.getAuthenticationOf((Queue.Task) item) : ACL.SYSTEM, + Collections.emptyList() + ), + CredentialsMatchers.withId(restApiKeyId) + ); + + if (credentials == null) { + throw new IllegalStateException("No credentials found for API key provided"); + } + + final MablRestApiClient client = + createMablRestApiClient( + credentials.getSecret(), + disableSslVerification + ); + return getApplicationIdItems(client); } private ListBoxModel getApplicationIdItems( @@ -393,7 +437,7 @@ private ListBoxModel getApplicationIdItems( String organizationId = apiKeyResult.organization_id; GetApplicationsResult applicationsResult = client.getApplicationsResult(organizationId); - items.add("", ""); + items.add("- none -", ""); for (GetApplicationsResult.Application application : applicationsResult.applications) { items.add(application.name, application.id); } @@ -406,23 +450,48 @@ private ListBoxModel getApplicationIdItems( return getSelectValidApiKeyListBoxModel(); } + @POST public ListBoxModel doFillEnvironmentIdItems( @QueryParameter String restApiKeyId, @QueryParameter boolean disableSslVerification, - @QueryParameter String apiBaseUrl, - @QueryParameter String appBaseUrl + @AncestorInPath Item item ) { + Jenkins.get().checkPermission(Job.CONFIGURE); if (StringUtils.isBlank(restApiKeyId)) { return getSelectValidApiKeyListBoxModel(); } - final Secret secretKey = getRestApiSecret(restApiKeyId); - if (secretKey != null) { - final MablRestApiClient client = createMablRestApiClient( - secretKey, disableSslVerification, apiBaseUrl, appBaseUrl); - return getEnvironmentIdItems(client); + if (item == null) { + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + return new ListBoxModel(); + } + } else { + if (!item.hasPermission(Item.EXTENDED_READ) + && !item.hasPermission(CredentialsProvider.USE_ITEM)) { + return new ListBoxModel(); + } + } + + StringCredentials credentials = CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentials( + StringCredentials.class, + item, + item instanceof Queue.Task ? Tasks.getAuthenticationOf((Queue.Task) item) : ACL.SYSTEM, + Collections.emptyList() + ), + CredentialsMatchers.withId(restApiKeyId) + ); + + if (credentials == null) { + throw new IllegalStateException("No credentials found for API key provided"); } - return new ListBoxModel(); + + final MablRestApiClient client = + createMablRestApiClient( + credentials.getSecret(), + disableSslVerification + ); + return getEnvironmentIdItems(client); } private ListBoxModel getEnvironmentIdItems(final MablRestApiClient client) { @@ -436,7 +505,7 @@ private ListBoxModel getEnvironmentIdItems(final MablRestApiClient client) { String organizationId = apiKeyResult.organization_id; GetEnvironmentsResult environmentsResult = client.getEnvironmentsResult(organizationId); - items.add("", ""); + items.add("- none -", ""); for (GetEnvironmentsResult.Environment environment : environmentsResult.environments) { items.add(environment.name, environment.id); } @@ -472,29 +541,40 @@ private static ListBoxModel getSelectValidApiKeyListBoxModel() { private static MablRestApiClient createMablRestApiClient( final Secret key, - final boolean disableSslVerification, - final String apiBaseUrl, - final String appBaseUrl + final boolean disableSslVerification ) { return new MablRestApiClientImpl( - StringUtils.isEmpty(apiBaseUrl) ? DEFAULT_MABL_API_BASE_URL : apiBaseUrl, + MABL_API_BASE_URL, key, - StringUtils.isEmpty(appBaseUrl) ? DEFAULT_MABL_APP_BASE_URL : appBaseUrl, + MABL_APP_BASE_URL, disableSslVerification); } } public @Nonnull static MablRestApiClient createMablRestApiClient( final String restApiKeyId, final boolean disableSslVerification, - final String apiBaseUrl, - final String appBaseUrl + final Job job ) { + StringCredentials credentials = CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentials( + StringCredentials.class, + job, + job instanceof Queue.Task ? Tasks.getAuthenticationOf((Queue.Task) job) : ACL.SYSTEM, + Collections.emptyList() + ), + CredentialsMatchers.withId(restApiKeyId) + ); + + if (credentials == null) { + throw new IllegalStateException("No credentials found for API key provided"); + } + return new MablRestApiClientImpl( - StringUtils.isEmpty(apiBaseUrl) ? DEFAULT_MABL_API_BASE_URL : apiBaseUrl, - getRestApiSecret(restApiKeyId), - StringUtils.isEmpty(appBaseUrl) ? DEFAULT_MABL_APP_BASE_URL : appBaseUrl, + MABL_API_BASE_URL, + credentials.getSecret(), + MABL_APP_BASE_URL, disableSslVerification); } -} \ No newline at end of file +}
src/main/java/com/mabl/integration/jenkins/MablStepConstants.java+3 −3 modified@@ -30,8 +30,8 @@ public class MablStepConstants { // Label for build steps drop down list static final String BUILD_STEP_DISPLAY_NAME = "Run mabl tests"; - public static final String DEFAULT_MABL_API_BASE_URL = "https://api.mabl.com"; - public static final String DEFAULT_MABL_APP_BASE_URL = "https://app.mabl.com"; + public static final String MABL_API_BASE_URL = "https://api.mabl.com"; + public static final String MABL_APP_BASE_URL = "https://app.mabl.com"; static final int EXECUTION_TIMEOUT_SECONDS = 6 * 3600; static final int REQUEST_TIMEOUT_MILLISECONDS = 10 * 60 * 1000; static final long EXECUTION_STATUS_POLLING_INTERNAL_MILLISECONDS = 10000; @@ -61,4 +61,4 @@ static String getPluginVersion() { } catch (IOException ignored) {} return PLUGIN_VERSION_UNKNOWN; } -} \ No newline at end of file +}
src/main/java/com/mabl/integration/jenkins/validation/MablStepBuilderValidator.java+7 −12 modified@@ -1,13 +1,12 @@ package com.mabl.integration.jenkins.validation; import com.mabl.integration.jenkins.MablRestApiClient; +import hudson.model.Job; import hudson.util.FormValidation; import java.io.IOException; import static com.mabl.integration.jenkins.MablStepBuilder.createMablRestApiClient; -import static com.mabl.integration.jenkins.MablStepConstants.DEFAULT_MABL_API_BASE_URL; -import static com.mabl.integration.jenkins.MablStepConstants.DEFAULT_MABL_APP_BASE_URL; import static com.mabl.integration.jenkins.MablStepConstants.FORM_API_KEY_LABEL; import static com.mabl.integration.jenkins.MablStepConstants.FORM_APPLICATION_ID_LABEL; import static com.mabl.integration.jenkins.MablStepConstants.FORM_ENVIRONMENT_ID_LABEL; @@ -32,10 +31,10 @@ public class MablStepBuilderValidator { public static FormValidation validateForm( String restApiKeyName, String environmentId, - String applicationId + String applicationId, + Job job ) { - return validateForm(restApiKeyName, environmentId, applicationId, false, - DEFAULT_MABL_API_BASE_URL, DEFAULT_MABL_APP_BASE_URL); + return validateForm(restApiKeyName, environmentId, applicationId, false, job); } /** @@ -45,17 +44,14 @@ public static FormValidation validateForm( * @param environmentId prospective environment identifier * @param applicationId prospective application identifier * @param disableSslVerification prospective flag to indicate if SSL verification should be disabled - * @param apiBaseUrl base URL for API (not user-visible) - * @param appBaseUrl base URL for the ap (not user-visible) * @return validation result */ public static FormValidation validateForm( String restApiKeyId, String environmentId, String applicationId, boolean disableSslVerification, - String apiBaseUrl, - String appBaseUrl + Job job ) { try { @@ -90,8 +86,7 @@ public static FormValidation validateForm( client = createMablRestApiClient( restApiKeyClean, disableSslVerification, - apiBaseUrl, - appBaseUrl + job ); client.checkConnection(); } catch (IOException e) { @@ -103,4 +98,4 @@ public static FormValidation validateForm( return error("Client error : " + e.getMessage()); } } -} \ No newline at end of file +}
src/main/resources/com/mabl/integration/jenkins/MablStepBuilder/config.jelly+4 −8 modified@@ -2,7 +2,9 @@ <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:c="/lib/credentials" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <f:entry title="${%API Key Secret}" field="restApiKeyId"> - <c:select includeUser="true"/> + <c:select includeUser="true"> + <option value="">- none -</option> + </c:select> </f:entry> <f:entry title="${%Application}" field="applicationId"> @@ -30,13 +32,7 @@ <f:checkbox default="false" /> </f:entry> </f:advanced> - <f:invisibleEntry> - <f:textbox field="apiBaseUrl" default="https://api.mabl.com"/> - </f:invisibleEntry> - <f:invisibleEntry> - <f:textbox field="appBaseUrl" default="https://app.mabl.com"/> - </f:invisibleEntry> <f:validateButton title="${%Validate}" progress="${%Validating...}" - method="validateForm" with="restApiKeyId,applicationId,environmentId,disableSslVerification,apiBaseUrl,appBaseUrl" /> + method="validateForm" with="restApiKeyId,applicationId,environmentId,disableSslVerification" /> </j:jelly>
src/test/java/com/mabl/integration/jenkins/MablStepDescriptorTest.java+72 −24 modified@@ -7,27 +7,33 @@ import hudson.util.FormValidation; import hudson.util.ListBoxModel; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; -import org.mockito.MockedStatic; +import org.jvnet.hudson.test.JenkinsRule; import java.util.Collection; import static com.cloudbees.plugins.credentials.CredentialsProvider.USE_ITEM; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.mockito.Mockito.mockStatic; import static org.junit.Assert.assertEquals; public class MablStepDescriptorTest { private MablStepBuilder.MablStepDescriptor mablStepDescriptor; + @Rule + public JenkinsRule j = new JenkinsRule(); + @Before public void setup() { mablStepDescriptor = mock(MablStepBuilder.MablStepDescriptor.class); doNothing().when(mablStepDescriptor).load(); + // create a dummy security realm + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); } @Test @@ -146,60 +152,102 @@ public void testCheckMablBranch_InValid() @Test public void testDoFillApplicationIdItems_NoApiKey() { + Item item = new AbstractItem(null, "testItem") { + @Override + public Collection<? extends Job> getAllJobs() { + return null; + } + + @Override + public boolean hasPermission(Permission permission) { + return true; + } + }; + when(mablStepDescriptor.doFillApplicationIdItems( - null, false, "https://api.mabl.com", "https://app.mabl.com")).thenCallRealMethod(); - ListBoxModel model = mablStepDescriptor.doFillApplicationIdItems(null, false, "https://api.mabl.com", "https://app.mabl.com"); + null, false, item)).thenCallRealMethod(); + ListBoxModel model = mablStepDescriptor.doFillApplicationIdItems(null, false, item); assertEquals(1, model.size()); assertEquals("Select a valid API key", model.iterator().next().value); when(mablStepDescriptor.doFillApplicationIdItems( - "", false, "https://api.mabl.com", "https://app.mabl.com")).thenCallRealMethod(); - model = mablStepDescriptor.doFillApplicationIdItems("", false, "https://api.mabl.com", "https://app.mabl.com"); + "", false, item)).thenCallRealMethod(); + model = mablStepDescriptor.doFillApplicationIdItems("", false, item); assertEquals(1, model.size()); assertEquals("Select a valid API key", model.iterator().next().value); } @Test public void testDoFillApplicationIdItems_NoSecret() { - when(mablStepDescriptor.doFillApplicationIdItems( - "invalid-key", false, "https://api.mabl.com", "https://app.mabl.com")).thenCallRealMethod(); + Item item = new AbstractItem(null, "testItem") { + @Override + public Collection<? extends Job> getAllJobs() { + return null; + } - try (MockedStatic<MablStepBuilder> mocked = mockStatic(MablStepBuilder.class)) { - mocked.when(() -> MablStepBuilder.getRestApiSecret("invalid-key")).thenReturn(null); + @Override + public boolean hasPermission(Permission permission) { + return true; + } + }; + + when(mablStepDescriptor.doFillApplicationIdItems( + "invalid-key", false, item)).thenCallRealMethod(); - ListBoxModel model = mablStepDescriptor.doFillApplicationIdItems("invalid-key", false, "https://api.mabl.com", "https://app.mabl.com"); - assertEquals(0, model.size()); - } + assertThrows( + IllegalStateException.class, + () -> mablStepDescriptor.doFillApplicationIdItems("invalid-key", false, item)); } @Test public void testDoFillEnvironmentIdItems_NoApiKey() { + Item item = new AbstractItem(null, "testItem") { + @Override + public Collection<? extends Job> getAllJobs() { + return null; + } + + @Override + public boolean hasPermission(Permission permission) { + return true; + } + }; + when(mablStepDescriptor.doFillEnvironmentIdItems( - null, false, "https://api.mabl.com", "https://app.mabl.com")).thenCallRealMethod(); - ListBoxModel model = mablStepDescriptor.doFillEnvironmentIdItems(null, false, "https://api.mabl.com", "https://app.mabl.com"); + null, false, item)).thenCallRealMethod(); + ListBoxModel model = mablStepDescriptor.doFillEnvironmentIdItems(null, false, item); assertEquals(1, model.size()); assertEquals("Select a valid API key", model.iterator().next().value); when(mablStepDescriptor.doFillEnvironmentIdItems( - "", false, "https://api.mabl.com", "https://app.mabl.com")).thenCallRealMethod(); - model = mablStepDescriptor.doFillEnvironmentIdItems("", false, "https://api.mabl.com", "https://app.mabl.com"); + "", false, item)).thenCallRealMethod(); + model = mablStepDescriptor.doFillEnvironmentIdItems("", false, item); assertEquals(1, model.size()); assertEquals("Select a valid API key", model.iterator().next().value); } @Test public void testDoFillEnvironmentIdItems_NoSecret() { - when(mablStepDescriptor.doFillEnvironmentIdItems( - "invalid-key", false, "https://api.mabl.com", "https://app.mabl.com")).thenCallRealMethod(); + Item item = new AbstractItem(null, "testItem") { + @Override + public Collection<? extends Job> getAllJobs() { + return null; + } + + @Override + public boolean hasPermission(Permission permission) { + return true; + } + }; - try (MockedStatic<MablStepBuilder> mocked = mockStatic(MablStepBuilder.class)) { - mocked.when(() -> MablStepBuilder.getRestApiSecret("invalid-key")).thenReturn(null); + when(mablStepDescriptor.doFillEnvironmentIdItems( + "invalid-key", false, item)).thenCallRealMethod(); - ListBoxModel model = mablStepDescriptor.doFillEnvironmentIdItems("invalid-key", false, "https://api.mabl.com", "https://app.mabl.com"); - assertEquals(0, model.size()); - } + assertThrows( + IllegalStateException.class, + () -> mablStepDescriptor.doFillEnvironmentIdItems("invalid-key", false, item)); } @Test
src/test/java/com/mabl/integration/jenkins/validation/MablBuildValidatorTest.java+32 −22 modified@@ -2,18 +2,19 @@ import com.mabl.integration.jenkins.MablRestApiClient; import com.mabl.integration.jenkins.MablStepBuilder; -import com.mabl.integration.jenkins.MablStepConstants; import com.mabl.integration.jenkins.domain.CreateDeploymentProperties; import com.mabl.integration.jenkins.domain.CreateDeploymentResult; import com.mabl.integration.jenkins.domain.ExecutionResult; import com.mabl.integration.jenkins.domain.GetApiKeyResult; import com.mabl.integration.jenkins.domain.GetApplicationsResult; import com.mabl.integration.jenkins.domain.GetEnvironmentsResult; -import com.mabl.integration.jenkins.domain.GetEnvironmentsResultTest; import com.mabl.integration.jenkins.domain.GetLabelsResult; -import com.mabl.integration.jenkins.domain.GetLabelsResultTest; +import hudson.model.FreeStyleProject; import hudson.util.FormValidation; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; import org.mockito.MockedStatic; import java.io.IOException; @@ -32,17 +33,26 @@ * Unit test runner */ public class MablBuildValidatorTest { + FreeStyleProject freeStyleProject; + + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + @Before + public void setupTest() { + freeStyleProject = jenkinsRule.jenkins.getItemByFullName("my-freestyle", FreeStyleProject.class); + } @Test public void validateGoodAllFieldsForm() { - try (MockedStatic<MablStepBuilder> mocked = mockStatic(MablStepBuilder.class)) { mockNoopRestApiClient(mocked); final FormValidation actual = validateForm( "sample-key-id", "sample-environment-id", - "sample-application-id" + "sample-application-id", + freeStyleProject ); assertEquals(actual.kind, OK); @@ -51,14 +61,14 @@ public void validateGoodAllFieldsForm() { @Test public void validateGoodEnvironmentOnlyForm() { - try (MockedStatic<MablStepBuilder> mocked = mockStatic(MablStepBuilder.class)) { mockNoopRestApiClient(mocked); final FormValidation actual = validateForm( "sample-key-id", "sample-environment-id", - null + null, + freeStyleProject ); assertEquals(actual.kind, OK); @@ -68,13 +78,13 @@ public void validateGoodEnvironmentOnlyForm() { @Test public void validateGoodApplicationOnlyForm() { - try (MockedStatic<MablStepBuilder> mocked = mockStatic(MablStepBuilder.class)) { mockNoopRestApiClient(mocked); final FormValidation actual = validateForm( "sample-key-id", null, - "sample-application-id" + "sample-application-id", + freeStyleProject ); assertEquals(actual.kind, OK); @@ -83,11 +93,11 @@ public void validateGoodApplicationOnlyForm() { @Test public void validateBadNoEnvironmentOrApplicationForm() { - final FormValidation actual = validateForm( "sample-key-id", null, - null + null, + freeStyleProject ); assertEquals(ERROR, actual.kind); @@ -99,11 +109,11 @@ public void validateBadNoEnvironmentOrApplicationForm() { @Test public void validateBadNoEnvironmentIdInWrongFieldApplicationForm() { - final FormValidation actual = validateForm( "sample-key-id", "sample-a", - null + null, + freeStyleProject ); assertEquals(ERROR, actual.kind); @@ -115,11 +125,11 @@ public void validateBadNoEnvironmentIdInWrongFieldApplicationForm() { @Test public void validateBadNoApplicationIdInWrongFieldApplicationForm() { - final FormValidation actual = validateForm( "sample-key-id", null, - "sample-e" + "sample-e", + freeStyleProject ); assertEquals(ERROR, actual.kind); @@ -131,11 +141,11 @@ public void validateBadNoApplicationIdInWrongFieldApplicationForm() { @Test public void validateBadNoEnvironmentOrApplicationWhiteSpaceForm() { - final FormValidation actual = validateForm( "sample-key-id", " ", - "\t" + "\t", + freeStyleProject ); assertEquals(ERROR, actual.kind); @@ -147,11 +157,11 @@ public void validateBadNoEnvironmentOrApplicationWhiteSpaceForm() { @Test public void validateBadNoRestApiKeyForm() { - final FormValidation actual = validateForm( null, null, - null + null, + freeStyleProject ); assertEquals(ERROR, actual.kind); @@ -161,11 +171,11 @@ public void validateBadNoRestApiKeyForm() { @Test public void validateBadRestApiKey() { - final FormValidation actual = validateForm( "key:invalid-key", null, - null + null, + freeStyleProject ); assertEquals(ERROR, actual.kind); @@ -175,7 +185,7 @@ public void validateBadRestApiKey() { private void mockNoopRestApiClient(MockedStatic<MablStepBuilder> mocked) { mocked.when(() -> MablStepBuilder.createMablRestApiClient( - "sample-key-id", false, MablStepConstants.DEFAULT_MABL_API_BASE_URL, MablStepConstants.DEFAULT_MABL_APP_BASE_URL)).thenReturn( + "sample-key-id", false, freeStyleProject)).thenReturn( new MablRestApiClient() { @Override public CreateDeploymentResult createDeploymentEvent(String environmentId, String applicationId, String labels, String mablBranch, CreateDeploymentProperties properties) throws IOException {
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-m9jj-p947-m8xvghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-37953ghsaADVISORY
- www.jenkins.io/security/advisory/2023-07-12/ghsavendor-advisoryWEB
- www.openwall.com/lists/oss-security/2023/07/12/2ghsaWEB
- github.com/jenkinsci/mabl-integration-plugin/commit/4e21f498b0704d62f5a6114fcd8c274a5f440b18ghsaWEB
News mentions
1- Jenkins Security Advisory 2023-07-12Jenkins Security Advisories · Jul 12, 2023