VYPR
High severityNVD Advisory· Published Mar 9, 2020· Updated Aug 4, 2024

CVE-2020-2144

CVE-2020-2144

Description

Jenkins Rundeck Plugin before 3.6.7 does not disable XML external entity processing, enabling XXE attacks via crafted webhook payloads.

AI Insight

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

Jenkins Rundeck Plugin before 3.6.7 does not disable XML external entity processing, enabling XXE attacks via crafted webhook payloads.

Vulnerability

Overview

The Jenkins Rundeck Plugin, versions 3.6.6 and earlier, fails to configure its XML parser to disable XML external entity (XXE) processing. This security flaw exists in the WebHookListener component, which processes incoming XML notifications from Rundeck. The plugin's XML parser is not set to protect against XXE, allowing external entities to be resolved during parsing [1][2].

Attack

Vector

An attacker can exploit this vulnerability by sending a crafted XML payload to the Jenkins plugin's webhook endpoint. The payload includes a malicious DOCTYPE declaration that defines external entities pointing to attacker-controlled resources. As shown in the provided test case, the parser will attempt to fetch external DTDs and entities, leading to XXE processing [4]. No authentication is required to trigger the webhook listener, making the attack surface wide for any Jenkins instance that has the Rundeck plugin installed and the webhook URL exposed.

Impact

Successful exploitation of this XXE vulnerability can allow an attacker to read arbitrary files on the Jenkins controller file system, perform server-side request forgery (SSRF) to access internal network resources, or potentially cause a denial of service through entity expansion. Given the plugin's role in integrating with Rundeck, sensitive deployment and configuration data could be exposed [1][2].

Mitigation

Jenkins released Rundeck Plugin version 3.6.7 which fixes the vulnerability by properly configuring the XML parser to disable external entity processing. Users are strongly encouraged to update to this latest version immediately. As stated in the security advisory, this is one of multiple plugins addressed in the March 2020 Jenkins security update [1]. No workaround other than patching is available.

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:rundeckMaven
< 3.6.73.6.7

Affected products

2

Patches

1
9222a2101d99

[SECURITY-1702]

https://github.com/jenkinsci/rundeck-pluginLuis ToledoMar 4, 2020via ghsa
3 files changed · +241 19
  • src/main/java/org/jenkinsci/plugins/rundeck/util/ParserXML.java+46 0 added
    @@ -0,0 +1,46 @@
    +package org.jenkinsci.plugins.rundeck.util;
    +
    +import org.dom4j.Document;
    +import org.dom4j.DocumentException;
    +import org.dom4j.Node;
    +import org.dom4j.io.SAXReader;
    +import org.rundeck.api.RundeckApiException;
    +import org.xml.sax.SAXException;
    +import java.io.InputStream;
    +
    +public class ParserXML {
    +
    +    public static Document loadDocument(InputStream inputStream) throws RundeckApiException {
    +
    +        SAXReader reader = new SAXReader();
    +
    +        try {
    +            reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
    +            reader.setFeature("http://xml.org/sax/features/external-general-entities", false);
    +            reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
    +
    +        } catch (SAXException e) {
    +            throw new RundeckApiException(e.getMessage());
    +        }
    +
    +        reader.setEncoding("UTF-8");
    +
    +        Document document;
    +        try {
    +            document = reader.read(inputStream);
    +        } catch (DocumentException ex) {
    +            throw new RundeckApiException("Failed to read Rundeck response: " + ex.getMessage());
    +        }
    +
    +        document.setXMLEncoding("UTF-8");
    +        Node result = document.selectSingleNode("result");
    +        if (result != null) {
    +            Boolean failure = Boolean.valueOf(result.valueOf("@error"));
    +            if (failure) {
    +                throw new RundeckApiException(result.valueOf("error/message"));
    +            }
    +        }
    +
    +        return document;
    +    }
    +}
    
  • src/main/java/org/jenkinsci/plugins/rundeck/WebHookListener.java+31 19 modified
    @@ -6,11 +6,12 @@
     import javax.servlet.http.HttpServletResponse;
     import org.apache.commons.io.IOUtils;
     import org.dom4j.Document;
    +import org.jenkinsci.plugins.rundeck.util.ParserXML;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
    +import org.kohsuke.stapler.interceptor.RequirePOST;
     import org.rundeck.api.domain.RundeckExecution;
     import org.rundeck.api.parser.ExecutionParser;
    -import org.rundeck.api.parser.ParserHelper;
     
     /**
      * Listener for Rundeck WebHook notifications (see http://rundeck.org/docs/manual/jobs.html#webhooks), will trigger a
    @@ -20,27 +21,38 @@
      */
     public class WebHookListener {
     
    +    @RequirePOST
         public void doIndex(StaplerRequest request, StaplerResponse response) throws IOException {
    +
             // read request body / parse Rundeck execution
    -        Document document = ParserHelper.loadDocument(request.getInputStream());
    -        IOUtils.closeQuietly(request.getInputStream());
    -        ExecutionParser parser = new ExecutionParser("notification/executions/execution");
    -        RundeckExecution execution = parser.parseXmlNode(document);
    -
    -        // write a basic response
    -        response.setStatus(HttpServletResponse.SC_OK);
    -        response.setContentType("text/plain");
    -        //response.getWriter().append("Thanks");
    -
    -        // notify all registered triggers
    -        for (AbstractProject<?, ?> job : Hudson.getInstance().getAllItems(AbstractProject.class)) {
    -            RundeckTrigger trigger = job.getTrigger(RundeckTrigger.class);
    -            if (trigger != null) {
    -                response.getWriter().append("[\"Triggering:\" : \""+job.getFullDisplayName()+"\"\n");
    -                response.getWriter().append("\"Execution\" : \"" + execution.getJob()+"\"]\n");
    -                trigger.onNotification(execution);
    +        Document document = null;
    +
    +        try{
    +            document = ParserXML.loadDocument(request.getInputStream());
    +
    +            IOUtils.closeQuietly(request.getInputStream());
    +            ExecutionParser parser = new ExecutionParser("notification/executions/execution");
    +            RundeckExecution execution = parser.parseXmlNode(document);
    +
    +            // write a basic response
    +            response.setStatus(HttpServletResponse.SC_OK);
    +            response.setContentType("text/plain");
    +
    +            // notify all registered triggers
    +            for (AbstractProject<?, ?> job : Hudson.getInstance().getAllItems(AbstractProject.class)) {
    +                RundeckTrigger trigger = job.getTrigger(RundeckTrigger.class);
    +                if (trigger != null) {
    +                    response.getWriter().append("[\"Triggering:\" : \""+job.getFullDisplayName()+"\"\n");
    +                    response.getWriter().append("\"Execution\" : \"" + execution.getJob()+"\"]\n");
    +                    trigger.onNotification(execution);
    +                }
                 }
    +        }catch (Exception e){
    +            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    +            response.setContentType("text/plain");
    +            response.getWriter().append(e.getMessage());
             }
    +
         }
     
    -}
    \ No newline at end of file
    +}
    
  • src/test/java/org/jenkinsci/plugins/rundeck/WebHookListenerTest.java+164 0 added
    @@ -0,0 +1,164 @@
    +package org.jenkinsci.plugins.rundeck;
    +
    +
    +import mockit.*;
    +import org.junit.Rule;
    +import org.junit.Test;
    +import org.jvnet.hudson.test.JenkinsRule;
    +import org.kohsuke.stapler.StaplerRequest;
    +import org.kohsuke.stapler.StaplerResponse;
    +
    +import javax.servlet.ReadListener;
    +import javax.servlet.ServletInputStream;
    +import java.io.ByteArrayInputStream;
    +import java.io.IOException;
    +import java.io.InputStream;
    +import java.io.PrintWriter;
    +import java.nio.charset.StandardCharsets;
    +
    +import static org.junit.Assert.assertEquals;
    +
    +
    +public class WebHookListenerTest {
    +    @Rule public JenkinsRule jenkins = new JenkinsRule();
    +
    +    @Test
    +    public void testInvalidEntities() throws IOException {
    +
    +        WebHookListener listener = new WebHookListener();
    +
    +        final String payload = "<?xml version=\"1.0\"?>\n" +
    +                "<!DOCTYPE ANY[\n" +
    +                "<!ENTITY % remote SYSTEM \"http://127.0.0.1:8000/test.dtd\">  \n" +
    +                "%remote;\n" +
    +                "%all;\n" +
    +                "]>\n" +
    +                "<root>&send;</root>";
    +
    +        InputStream data = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8));
    +        final ServletInputStream servletInputStream=new DelegatingServletInputStream(data);
    +
    +        listener.doIndex(
    +                new MockUp<StaplerRequest>() {
    +                    @Mock
    +                    public ServletInputStream getInputStream(){
    +                        return servletInputStream;
    +                    }
    +                }.getMockInstance(),
    +                new MockUp<StaplerResponse>() {
    +                    @Mock
    +                    public void setStatus(int num){
    +                        assertEquals(num, 400);
    +                    }
    +
    +                    @Mock
    +                    public PrintWriter getWriter(){
    +                        return new PrintWriter(System.out);
    +                    }
    +
    +                }.getMockInstance()
    +        );
    +    }
    +
    +    @Test
    +    public void testValidData() throws IOException {
    +
    +        WebHookListener listener = new WebHookListener();
    +
    +        final String payload = "<notification trigger='success' status='success' executionId='37'>\n" +
    +                "\n" +
    +                "<executions count='1'>\n" +
    +                "  <execution id='176300' href='http://rundeck/api/34/execution/176300' permalink='http://rundeck/project/Support/execution/show/176300' status='succeeded' project='Support'>\n" +
    +                "    <user>admin</user>\n" +
    +                "    <date-started unixtime='1581081053000'>2020-02-07T13:10:53Z</date-started>\n" +
    +                "    <date-ended unixtime='1581081054000'>2020-02-07T13:10:54Z</date-ended>\n" +
    +                "    <job id='6fa68fe1-6894-477c-a997-ba1004b4ae83' averageDuration='1326' href='http://rundeck/api/34/job/6fa68fe1-6894-477c-a997-ba1004b4ae83' permalink='http://rundeck/project/Support/job/show/6fa68fe1-6894-477c-a997-ba1004b4ae83'>\n" +
    +                "      <name>jenkins-notification</name>\n" +
    +                "      <group></group>\n" +
    +                "      <project>Support</project>\n" +
    +                "      <description></description>\n" +
    +                "    </job>\n" +
    +                "    <description>ls -lrt</description>\n" +
    +                "    <argstring />\n" +
    +                "    <serverUUID>5c9e3bd4-50ed-4563-9dbd-f3f331326579</serverUUID>\n" +
    +                "    <successfulNodes>\n" +
    +                "      <node name='localhost' />\n" +
    +                "    </successfulNodes>\n" +
    +                "  </execution>\n" +
    +                "</executions>\n" +
    +                "\n" +
    +                "</notification>";
    +
    +        InputStream data = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8));
    +        final ServletInputStream servletInputStream=new DelegatingServletInputStream(data);
    +
    +        listener.doIndex(
    +                new MockUp<StaplerRequest>() {
    +                    @Mock
    +                    public ServletInputStream getInputStream(){
    +                        return servletInputStream;
    +                    }
    +                }.getMockInstance(),
    +                new MockUp<StaplerResponse>() {
    +                    @Mock
    +                    public void setStatus(int num){
    +                        assertEquals(num, 200);
    +                    }
    +
    +                    @Mock
    +                    public PrintWriter getWriter(){
    +                        return new PrintWriter(System.out);
    +                    }
    +
    +                }.getMockInstance()
    +        );
    +    }
    +
    +}
    +
    +class DelegatingServletInputStream extends ServletInputStream {
    +
    +    private final InputStream sourceStream;
    +
    +
    +    /**
    +     * Create a DelegatingServletInputStream for the given source stream.
    +     * @param sourceStream the source stream (never <code>null</code>)
    +     */
    +    public DelegatingServletInputStream(InputStream sourceStream) {
    +        this.sourceStream = sourceStream;
    +    }
    +
    +    /**
    +     * Return the underlying source stream (never <code>null</code>).
    +     */
    +    public final InputStream getSourceStream() {
    +        return this.sourceStream;
    +    }
    +
    +
    +    public int read() throws IOException {
    +        return this.sourceStream.read();
    +    }
    +
    +    public void close() throws IOException {
    +        super.close();
    +        this.sourceStream.close();
    +    }
    +
    +    @Override
    +    public boolean isFinished() {
    +        return false;
    +    }
    +
    +    @Override
    +    public boolean isReady() {
    +        return false;
    +    }
    +
    +    @Override
    +    public void setReadListener(ReadListener readListener) {
    +
    +    }
    +}
    +
    

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