Coordinated Disclosure Timeline

Summary

Jenkins MathWorks Polyspace Plugin 1.0.5 and earlier does not restrict a file path in a job parameter, allowing attackers with the Job/Configure permission to exfiltrate arbitrary files from the Jenkins controller by sending them in an email notification.

Product

Jenkins MathWorks Polyspace Plugin

Tested Version

1.0.5

Details

Arbitrary file read in PolyspacePostBuildActions.java (GHSL-2023-079)

The MathWorks Polyspace Jenkins plugin implements a post-build action in PolyspacePostBuildActions.java, which has a fileToAttach field:

src/main/java/com/mathworks/polyspace/jenkins/PolyspacePostBuildActions.java:72

@DataBoundSetter
public void setFileToAttach(String fileToAttach) {
  this.fileToAttach = fileToAttach;
}

This field is used when the action is executed, in the perform method, to attach a local file to an email notification:

public void perform(Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException
{
  if (sendToRecipients && (recipients != null) && !recipients.equals("")) {
    String attachSource = "";
    String attachName = "";
    if ((fileToAttach != null) && !fileToAttach.equals("")) {
      attachSource = workspace + File.separator + fileToAttach;
      attachName = getAttachName(attachSource);
    }
    sendMail(recipients, generateMailSubject(mailSubject, "", workspace, build), generateMailBody(mailBody, "", attachName, attachSource, workspace, build), attachSource, attachName);
  }
  // --snip--

It can be seen that fileToAttach is concatenated to workspace + File.separator without validation to build attachSource, which is then passed to getAttachName, and both are used in sendMail and generateMailBody.

sendMail creates the email object, attaches the file pointed by attachSource by creating a File object (which acts on the Jenkins controller), and sends it:

src/main/java/com/mathworks/polyspace/jenkins/PolyspacePostBuildActions.java:130

public void sendMail( @QueryParameter String sendMailTo,
                      @QueryParameter String subject,
                      @QueryParameter String text,
                      @QueryParameter String attachSource,
                      @QueryParameter String attachName
                    ) throws IOException
{
  try {
    String charset = Mailer.descriptor().getCharset();
    MimeMessage msg = new MimeMessage(Mailer.descriptor().createSession());
    // --snip--
    // create the message body, with the text followed by the file to attach if any
    Multipart multipart = new MimeMultipart();

    if (!attachName.equals("")) {
      File file = new File(attachSource);
      if (file.length()  > 10*1024*1024) {
        // size of the attachement is too large
        // do not attach, and add a message in the mail body
        text += "\n\nSize of the attached file is too large  -  Not attached\n";
      } else {
        MimeBodyPart attachmentBodyPart= new MimeBodyPart();
        DataSource source = new FileDataSource(attachSource);
        attachmentBodyPart.setDataHandler(new DataHandler(source));
        attachmentBodyPart.setFileName(attachName);
        multipart.addBodyPart(attachmentBodyPart);
      }
    }

    MimeBodyPart textBodyPart = new MimeBodyPart();
    textBodyPart.setText(text, charset);
    multipart.addBodyPart(textBodyPart);

    msg.setContent(multipart);
    Transport.send(msg);
  }
  // --snip--
}

There’s one caveat: note that getAttachName requires the file that is going to be attached to exist in the Jenkins controller (since it uses the File class directly), but attachSource is prefixed with the workspace path, which comes from the agent, so unless the job is running on the controller or the workspace path also exists in the controller, getAttachName returns an empty string, which prevents the file from being attached to the email:

src/main/java/com/mathworks/polyspace/jenkins/PolyspacePostBuildActions.java:119

private String getAttachName(String attachSource)
{
  if ((attachSource != null) && !attachSource.equals("")) {
    File f = new File(attachSource);
    if (f.exists() && !f.isDirectory()) {
      return f.getName();
    }
  }
  return "";
}

This also prevents the attach functionality from working properly in a distributed Jenkins environment.

This issue was found by the Uncontrolled data used in path expression CodeQL query.

Impact

This issue may lead to arbitrary file read.

Proof of concept

To exploit this vulnerability, the global configuration for the Polyspace plugin needs to have been set up (it doesn’t need to be correct), and a functional SMTP server needs to have been configured in Jenkins.

Then, an attacker with Job/Configure permissions could create a new job, set the Polyspace environment, add a Polyspace Notification post-build step, and configure the Attachment filename property as something like ../../../../../../../../etc/passwd.

As a proof of concept, Jenkins can be configured to use a local SMTP server at localhost:25, and the following command can be used to run one locally: sudo python3 -m smtpd -n -d localhost:25.

After that, when the job is executed, the SMTP server will receive an email containing the desired file (in this case /etc/passwd) as an attachment. In a real attack, since the attacker controls the recipients, they can just use an email they control, and the configured SMTP server will send the email there.

CVE

Resources

Credit

This issue was discovered and reported by the GitHub CodeQL team member @atorralba (Tony Torralba).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2023-079 in any communication regarding this issue.