VYPR
High severityNVD Advisory· Published Oct 23, 2019· Updated Aug 4, 2024

CVE-2019-10476

CVE-2019-10476

Description

Jenkins Zulip Plugin stored API keys in plain text in global config, allowing users with file system access to view credentials.

AI Insight

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

Jenkins Zulip Plugin stored API keys in plain text in global config, allowing users with file system access to view credentials.

Vulnerability

Description

The Jenkins Zulip Plugin, versions 1.1.0 and earlier, stored the Zulip API key as a plain text string in its global configuration file (jenkins.plugins.zulip.ZulipNotifier.xml) and in a legacy configuration file (hudson.plugins.humbug.HumbugNotifier.xml). This is a classic example of storing sensitive credentials in cleartext, making them accessible to anyone who can read the Jenkins master's file system [1][2].

Exploitation

An attacker does not need any special permissions within Jenkins itself to exploit this vulnerability; they only require access to the Jenkins master's file system. This could be achieved through other vulnerabilities, direct shell access, or by reading files via a compromised Jenkins agent. Once the configuration file is read, the API key is immediately visible as a plain text string [2][3].

Impact

With the exposed API key, an attacker can send messages to Zulip streams as the configured bot, potentially disrupting notifications, impersonating the bot, or leaking information. The Jenkins Security Advisory rates this vulnerability as Low severity (CVSS score not explicitly given but implied low) [2]. The impact is limited to the Zulip integration's functionality and does not directly compromise the Jenkins controller itself.

Mitigation

The vulnerability is fixed in Zulip Plugin version 1.1.1. The fix changes the apiKey field from a plain String to a hudson.util.Secret object, which encrypts the value when stored on disk [1]. Users should upgrade to version 1.1.1 or later. No workaround is available other than restricting file system access to the Jenkins master [2][4].

AI Insight generated on May 22, 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:zulipMaven
< 1.1.11.1.1

Affected products

2

Patches

1
2a9dd6c41c2d

SECURITY-1621 Store global config API key as Secret

https://github.com/jenkinsci/zulip-pluginbutchyyyyOct 7, 2019via ghsa
7 files changed · +56 20
  • src/main/java/jenkins/plugins/zulip/DescriptorImpl.java+19 5 modified
    @@ -9,6 +9,7 @@
     import hudson.model.AbstractProject;
     import hudson.tasks.BuildStepDescriptor;
     import hudson.tasks.Publisher;
    +import hudson.util.Secret;
     import hudson.util.XStream2;
     import jenkins.model.Jenkins;
     import net.sf.json.JSONObject;
    @@ -23,9 +24,11 @@ public class DescriptorImpl extends BuildStepDescriptor<Publisher> {
     
         private static final Logger logger = Logger.getLogger(DescriptorImpl.class.getName());
     
    +    private static final String OLD_CONFIG_FILE_NAME = "hudson.plugins.humbug.HumbugNotifier.xml";
    +
         private String url;
         private String email;
    -    private String apiKey;
    +    private Secret apiKey;
         private String stream;
         private String topic;
         private transient String hudsonUrl; // backwards compatibility
    @@ -40,7 +43,7 @@ public DescriptorImpl() {
             } else {
                 XStream2 xstream = new XStream2();
                 xstream.alias("hudson.plugins.humbug.DescriptorImpl", DescriptorImpl.class);
    -            XmlFile oldConfig = new XmlFile(xstream, new File(Jenkins.getInstance().getRootDir(),"hudson.plugins.humbug.HumbugNotifier.xml"));
    +            XmlFile oldConfig = new XmlFile(xstream, new File(Jenkins.getInstance().getRootDir(), OLD_CONFIG_FILE_NAME));
                 if (oldConfig.exists()) {
                     try {
                         oldConfig.unmarshal(this);
    @@ -67,11 +70,11 @@ public void setEmail(String email) {
             this.email = email;
         }
     
    -    public String getApiKey() {
    +    public Secret getApiKey() {
             return apiKey;
         }
     
    -    public void setApiKey(String apiKey) {
    +    public void setApiKey(Secret apiKey) {
             this.apiKey = apiKey;
         }
     
    @@ -115,12 +118,23 @@ public boolean isApplicable(Class<? extends AbstractProject> aClass) {
         public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
             url = (String) json.get("url");
             email = (String) json.get("email");
    -        apiKey = (String) json.get("apiKey");
    +        apiKey = Secret.fromString((String) json.get("apiKey"));
             stream = (String) json.get("stream");
             topic = (String) json.get("topic");
             jenkinsUrl = (String) json.get("jenkinsUrl");
             smartNotify = Boolean.TRUE.equals(json.get("smartNotify"));
             save();
    +
    +        // Cleanup the configuration file from previous plugin id - humbug
    +        File oldConfig = new File(Jenkins.getInstance().getRootDir(), OLD_CONFIG_FILE_NAME);
    +        if (oldConfig.exists()) {
    +            if (oldConfig.delete()) {
    +                logger.log(Level.INFO, "Old humbug configuration file successfully cleaned up.");
    +            } else {
    +                logger.log(Level.WARNING, "Failed to cleanup old humbug configuration file.");
    +            }
    +        }
    +
             return super.configure(req, json);
         }
     
    
  • src/main/java/jenkins/plugins/zulip/Zulip.java+3 2 modified
    @@ -10,6 +10,7 @@
     import java.util.logging.Logger;
     
     import hudson.ProxyConfiguration;
    +import hudson.util.Secret;
     import jenkins.model.Jenkins;
     import org.apache.commons.codec.binary.Base64;
     import org.apache.commons.httpclient.HttpClient;
    @@ -32,14 +33,14 @@ public class Zulip {
         private String apiKey;
         private static final Logger LOGGER = Logger.getLogger(Zulip.class.getName());
     
    -    public Zulip(String url, String email, String apiKey) {
    +    public Zulip(String url, String email, Secret apiKey) {
             super();
             if (url != null && url.length() > 0 && !url.endsWith("/") ) {
                 url = url + "/";
             }
             this.url = url;
             this.email = email;
    -        this.apiKey = apiKey;
    +        this.apiKey = Secret.toString(apiKey);
         }
     
         /**
    
  • src/main/resources/jenkins/plugins/zulip/ZulipNotifier/global.jelly+1 1 modified
    @@ -21,7 +21,7 @@
             <f:textbox name="email" value="${descriptor.getEmail()}" />
         </f:entry>
         <f:entry title="Zulip API Key" help="/plugin/zulip/help-globalConfig-apiKey.html">
    -        <f:textbox name="apiKey" value="${descriptor.getApiKey()}" />
    +        <f:password name="apiKey" value="${descriptor.getApiKey()}" />
         </f:entry>
         <f:entry title="Default Stream Name" help="/plugin/zulip/help-globalConfig-stream.html">
             <f:textbox name="stream" value="${descriptor.getStream()}" />
    
  • src/test/java/jenkins/plugins/zulip/IntegrationTest.java+2 1 modified
    @@ -3,6 +3,7 @@
     import com.gargoylesoftware.htmlunit.html.HtmlForm;
     import com.gargoylesoftware.htmlunit.html.HtmlPage;
     import hudson.model.FreeStyleProject;
    +import hudson.util.Secret;
     import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
     import org.jenkinsci.plugins.workflow.job.WorkflowJob;
     import org.junit.AfterClass;
    @@ -120,7 +121,7 @@ private void verifyGlobalConfig() {
             DescriptorImpl globalConfig = j.jenkins.getDescriptorByType(DescriptorImpl.class);
             assertEquals("ZulipUrl", globalConfig.getUrl());
             assertEquals("jenkins-bot@zulip.com", globalConfig.getEmail());
    -        assertEquals("secret", globalConfig.getApiKey());
    +        assertEquals("secret", Secret.toString(globalConfig.getApiKey()));
             assertEquals("defaultStream", globalConfig.getStream());
             assertEquals("defaultTopic", globalConfig.getTopic());
             assertTrue(globalConfig.isSmartNotify());
    
  • src/test/java/jenkins/plugins/zulip/ZulipNotifierTest.java+10 3 modified
    @@ -13,6 +13,7 @@
     import hudson.model.User;
     import hudson.scm.ChangeLogSet;
     import hudson.tasks.test.AbstractTestResultAction;
    +import hudson.util.Secret;
     import jenkins.model.Jenkins;
     import org.junit.Before;
     import org.junit.Test;
    @@ -31,7 +32,10 @@
     import static org.hamcrest.CoreMatchers.is;
     import static org.junit.Assert.assertEquals;
     import static org.junit.Assert.assertThat;
    +import static org.mockito.Matchers.any;
     import static org.mockito.Matchers.anyString;
    +import static org.mockito.Matchers.eq;
    +import static org.mockito.Matchers.same;
     import static org.mockito.Mockito.reset;
     import static org.mockito.Mockito.times;
     import static org.mockito.Mockito.verify;
    @@ -40,7 +44,7 @@
     
     @RunWith(PowerMockRunner.class)
     @PrepareForTest({Jenkins.class, User.class, ZulipNotifier.class, DescriptorImpl.class,
    -        AbstractBuild.class, Job.class})
    +        AbstractBuild.class, Job.class, Secret.class})
     public class ZulipNotifierTest {
     
         private static final int TOTAL_TEST_COUNT = 100;
    @@ -49,6 +53,9 @@ public class ZulipNotifierTest {
         @Mock
         private Jenkins jenkins;
     
    +    @Mock
    +    private Secret secret;
    +
         @Mock
         private Zulip zulip;
     
    @@ -99,7 +106,7 @@ public User answer(InvocationOnMock invocation) throws Throwable {
             });
             when(descMock.getUrl()).thenReturn("zulipUrl");
             when(descMock.getEmail()).thenReturn("jenkins-bot@zulip.com");
    -        when(descMock.getApiKey()).thenReturn("secret");
    +        when(descMock.getApiKey()).thenReturn(secret);
             when(descMock.getStream()).thenReturn("defaultStream");
             when(descMock.getTopic()).thenReturn("defaultTopic");
             PowerMockito.whenNew(DescriptorImpl.class).withAnyArguments().thenReturn(descMock);
    @@ -123,7 +130,7 @@ public String answer(InvocationOnMock invocation) throws Throwable {
         public void testShouldUseDefaults() throws Exception {
             ZulipNotifier notifier = new ZulipNotifier();
             notifier.perform(build, null, buildListener);
    -        verifyNew(Zulip.class).withArguments("zulipUrl", "jenkins-bot@zulip.com", "secret");
    +        verifyNew(Zulip.class).withArguments(eq("zulipUrl"), eq("jenkins-bot@zulip.com"), any(Secret.class));
             verify(envVars, times(2)).expand(expandCaptor.capture());
             assertThat("Should expand stream, topic and message", expandCaptor.getAllValues(),
                     is(Arrays.asList("defaultStream", "defaultTopic")));
    
  • src/test/java/jenkins/plugins/zulip/ZulipSendStepTest.java+9 3 modified
    @@ -6,6 +6,7 @@
     import hudson.model.Job;
     import hudson.model.Run;
     import hudson.model.TaskListener;
    +import hudson.util.Secret;
     import jenkins.model.Jenkins;
     import org.junit.Before;
     import org.junit.Test;
    @@ -23,20 +24,25 @@
     import static org.junit.Assert.assertEquals;
     import static org.junit.Assert.assertNull;
     import static org.junit.Assert.assertThat;
    +import static org.mockito.Matchers.any;
     import static org.mockito.Matchers.anyString;
    +import static org.mockito.Matchers.eq;
     import static org.mockito.Mockito.reset;
     import static org.mockito.Mockito.times;
     import static org.mockito.Mockito.verify;
     import static org.powermock.api.mockito.PowerMockito.verifyNew;
     import static org.powermock.api.mockito.PowerMockito.when;
     
     @RunWith(PowerMockRunner.class)
    -@PrepareForTest({Jenkins.class, ZulipSendStep.class})
    +@PrepareForTest({Jenkins.class, ZulipSendStep.class, Secret.class})
     public class ZulipSendStepTest {
     
         @Mock
         private Jenkins jenkins;
     
    +    @Mock
    +    private Secret secret;
    +
         @Mock
         private Zulip zulip;
     
    @@ -75,7 +81,7 @@ public void setUp() throws Exception {
             when(jenkins.getDescriptorByType(DescriptorImpl.class)).thenReturn(descMock);
             when(descMock.getUrl()).thenReturn("zulipUrl");
             when(descMock.getEmail()).thenReturn("jenkins-bot@zulip.com");
    -        when(descMock.getApiKey()).thenReturn("secret");
    +        when(descMock.getApiKey()).thenReturn(secret);
             when(descMock.getStream()).thenReturn("defaultStream");
             when(descMock.getTopic()).thenReturn("defaultTopic");
             when(run.getParent()).thenReturn(job);
    @@ -94,7 +100,7 @@ public void testShouldUseDefaults() throws Exception {
             ZulipSendStep sendStep = new ZulipSendStep();
             sendStep.setMessage("message");
             sendStep.perform(run, null, null, taskListener);
    -        verifyNew(Zulip.class).withArguments("zulipUrl", "jenkins-bot@zulip.com", "secret");
    +        verifyNew(Zulip.class).withArguments(eq("zulipUrl"), eq("jenkins-bot@zulip.com"), any(Secret.class));
             verify(envVars, times(3)).expand(expandCaptor.capture());
             assertThat("Should expand stream, topic and message", expandCaptor.getAllValues(),
                     is(Arrays.asList("defaultStream", "defaultTopic", "message")));
    
  • src/test/java/jenkins/plugins/zulip/ZulipTest.java+12 5 modified
    @@ -4,6 +4,7 @@
     import java.nio.charset.Charset;
     
     import com.google.common.net.HttpHeaders;
    +import hudson.util.Secret;
     import jenkins.model.Jenkins;
     import org.apache.commons.httpclient.NameValuePair;
     import org.junit.AfterClass;
    @@ -18,20 +19,24 @@
     import org.powermock.core.classloader.annotations.PrepareForTest;
     import org.powermock.modules.junit4.PowerMockRunner;
     
    +import static org.mockito.Matchers.any;
     import static org.mockserver.model.HttpRequest.request;
     import static org.mockserver.model.HttpResponse.response;
     import static org.mockserver.model.StringBody.exact;
     import static org.powermock.api.mockito.PowerMockito.when;
     
     @RunWith(PowerMockRunner.class)
    -@PrepareForTest(Jenkins.class)
    +@PrepareForTest({Jenkins.class, Secret.class})
     public class ZulipTest {
     
         private static ClientAndServer mockServer;
     
         @Mock
         private Jenkins jenkins;
     
    +    @Mock
    +    private Secret secret;
    +
         @BeforeClass
         public static void startMockServer() {
             mockServer = ClientAndServer.startClientAndServer(1080);
    @@ -46,13 +51,15 @@ public static void stopMockServer() {
         public void setUp() {
             PowerMockito.mockStatic(Jenkins.class);
             when(Jenkins.getInstance()).thenReturn(jenkins);
    +        PowerMockito.mockStatic(Secret.class);
    +        when(Secret.toString(any(Secret.class))).thenReturn("secret");
         }
     
         @Test
         public void testSendStreamMessage() throws Exception {
             mockServer.reset();
             mockServer.when(request().withPath("/api/v1/messages")).respond(response().withStatusCode(200));
    -        Zulip zulip = new Zulip("http://localhost:1080", "jenkins-bot@zulip.com", "secret");
    +        Zulip zulip = new Zulip("http://localhost:1080", "jenkins-bot@zulip.com", secret);
             zulip.sendStreamMessage("testStreamůř", "testTopic", "testMessage");
             mockServer.verify(
                     request()
    @@ -78,21 +85,21 @@ public void testSendStreamMessage() throws Exception {
         public void testFailGracefullyOnError() {
             mockServer.reset();
             mockServer.when(request().withPath("/api/v1/messages")).respond(response().withStatusCode(500));
    -        Zulip zulip = new Zulip("http://localhost:1080", "jenkins-bot@zulip.com", "secret");
    +        Zulip zulip = new Zulip("http://localhost:1080", "jenkins-bot@zulip.com", secret);
             // Test that this does not throw exception
             zulip.sendStreamMessage("testStream", "testTopic", "testMessage");
         }
     
         @Test
         public void testFailGracefullyWhenUnreachable() {
    -        Zulip zulip = new Zulip("http://localhost:1081", "jenkins-bot@zulip.com", "secret");
    +        Zulip zulip = new Zulip("http://localhost:1081", "jenkins-bot@zulip.com", secret);
             // Test that this does not throw exception
             zulip.sendStreamMessage("testStream", "testTopic", "testMessage");
         }
     
         @Test
         public void testFailGracefullyUnknonwHost() {
    -        Zulip zulip = new Zulip("http://unreachable:1080", "jenkins-bot@zulip.com", "secret");
    +        Zulip zulip = new Zulip("http://unreachable:1080", "jenkins-bot@zulip.com", secret);
             // Test that this does not throw exception
             zulip.sendStreamMessage("testStream", "testTopic", "testMessage");
         }
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.