diff --git a/CHANGELOG.md b/CHANGELOG.md index f6bcb1b..7f63ea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Removed +- Removed jdom dependency + - Address XXE in JDOM SAXBuilder [CVE-2021-33813](https://github.com/advisories/GHSA-2363-cqg2-863c) + - Use w3c dom instead + ## [1.17.0] - 2025-12-15 ### Added diff --git a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/AlertsFile.java b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/AlertsFile.java index f2a8f2a..fd5c587 100644 --- a/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/AlertsFile.java +++ b/subprojects/zap-clientapi/src/main/java/org/zaproxy/clientapi/core/AlertsFile.java @@ -3,7 +3,7 @@ * * ZAP is an HTTP/HTTPS proxy for assessing web application security. * - * Copyright 2012 The ZAP Development Team + * Copyright 2025 The ZAP Development Team * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,112 +23,248 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; +import java.rmi.RemoteException; import java.util.ArrayList; import java.util.List; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.JDOMException; -import org.jdom.input.SAXBuilder; -import org.jdom.output.Format; -import org.jdom.output.XMLOutputter; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; public class AlertsFile { + + /** + * Writes the alerts into an XML file with the following structure: + * + *
+     * <alerts>
+     *   <alertsFound alertsFound="N">
+     *     <alert .../>
+     *   </alertsFound>
+     *   <alertsNotFound alertsNotFound="M">
+     *     <alert .../>
+     *   </alertsNotFound>
+     *   <ignoredAlertsFound ignoredAlertsFound="K">
+     *     <alert .../>
+     *   </ignoredAlertsFound>
+     * </alerts>
+     * 
+ */ public static void saveAlertsToFile( List requireAlerts, List reportAlerts, List ignoredAlerts, File outputFile) - throws JDOMException, IOException { - Element alerts = new Element("alerts"); - Document alertsDocument = new Document(alerts); - alertsDocument.setRootElement(alerts); - if (reportAlerts.size() > 0) { - Element alertsFound = new Element("alertsFound"); - alertsFound.setAttribute("alertsFound", Integer.toString(reportAlerts.size())); - for (Alert alert : reportAlerts) { - createAlertXMLElements(alertsFound, alert); - } - alertsDocument.getRootElement().addContent(alertsFound); + throws IOException { + + if (requireAlerts == null) { + requireAlerts = new ArrayList<>(); + } + if (reportAlerts == null) { + reportAlerts = new ArrayList<>(); } + if (ignoredAlerts == null) { + ignoredAlerts = new ArrayList<>(); + } + + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document alertsDocument = builder.newDocument(); + + Element root = alertsDocument.createElement("alerts"); + alertsDocument.appendChild(root); - if (requireAlerts.size() > 0) { - Element alertsNotFound = new Element("alertsNotFound"); - alertsNotFound.setAttribute("alertsNotFound", Integer.toString(requireAlerts.size())); - for (Alert alert : requireAlerts) { - createAlertXMLElements(alertsNotFound, alert); + if (!reportAlerts.isEmpty()) { + Element alertsFound = alertsDocument.createElement("alertsFound"); + alertsFound.setAttribute("alertsFound", Integer.toString(reportAlerts.size())); + for (Alert alert : reportAlerts) { + createAlertXMLElements(alertsDocument, alertsFound, alert); + } + root.appendChild(alertsFound); } - alertsDocument.getRootElement().addContent(alertsNotFound); - } - if (ignoredAlerts.size() > 0) { - Element ignoredAlertsFound = new Element("ignoredAlertsFound"); - ignoredAlertsFound.setAttribute( - "ignoredAlertsFound", Integer.toString(ignoredAlerts.size())); - for (Alert alert : ignoredAlerts) { - createAlertXMLElements(ignoredAlertsFound, alert); + if (!requireAlerts.isEmpty()) { + Element alertsNotFound = alertsDocument.createElement("alertsNotFound"); + alertsNotFound.setAttribute( + "alertsNotFound", Integer.toString(requireAlerts.size())); + for (Alert alert : requireAlerts) { + createAlertXMLElements(alertsDocument, alertsNotFound, alert); + } + root.appendChild(alertsNotFound); } - alertsDocument.getRootElement().addContent(ignoredAlertsFound); - } - writeAlertsToFile(outputFile, alertsDocument); + if (!ignoredAlerts.isEmpty()) { + Element ignoredAlertsFound = alertsDocument.createElement("ignoredAlertsFound"); + ignoredAlertsFound.setAttribute( + "ignoredAlertsFound", Integer.toString(ignoredAlerts.size())); + for (Alert alert : ignoredAlerts) { + createAlertXMLElements(alertsDocument, ignoredAlertsFound, alert); + } + root.appendChild(ignoredAlertsFound); + } + + writeAlertsToFile(outputFile, alertsDocument); + + } catch (ParserConfigurationException | TransformerException e) { + throw new RuntimeException("Failed to save alerts to file: " + outputFile, e); + } } - private static void writeAlertsToFile(File outputFile, Document doc) throws IOException { + private static void writeAlertsToFile(File outputFile, Document doc) + throws IOException, TransformerException { - XMLOutputter xmlOutput = new XMLOutputter(); + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); - xmlOutput.setFormat(Format.getPrettyFormat()); try (OutputStream os = Files.newOutputStream(outputFile.toPath())) { - xmlOutput.output(doc, os); + DOMSource source = new DOMSource(doc); + StreamResult result = new StreamResult(os); + transformer.transform(source, result); System.out.println("alert xml report saved to: " + outputFile.getAbsolutePath()); } } - private static void createAlertXMLElements(Element alertsFound, Alert alert) { - Element alertElement = new Element("alert"); + private static void createAlertXMLElements(Document doc, Element alertsParent, Alert alert) { + + Element alertElement = doc.createElement("alert"); + if (alert.getName() != null) { alertElement.setAttribute("name", alert.getName()); // TODO Remove once alert attribute is no longer supported. alertElement.setAttribute("alert", alert.getName()); } - if (alert.getRisk() != null) alertElement.setAttribute("risk", alert.getRisk().name()); - if (alert.getUrl() != null) + + if (alert.getRisk() != null) { + alertElement.setAttribute("risk", alert.getRisk().name()); + } + + if (alert.getConfidence() != null) { alertElement.setAttribute("confidence", alert.getConfidence().name()); - if (alert.getUrl() != null) alertElement.setAttribute("url", alert.getUrl()); - if (alert.getParam() != null) alertElement.setAttribute("param", alert.getParam()); - if (alert.getOther() != null) alertElement.setAttribute("other", alert.getOther()); - if (alert.getAttack() != null) alertElement.setAttribute("attack", alert.getAttack()); - if (alert.getDescription() != null) + } + + if (alert.getUrl() != null) { + alertElement.setAttribute("url", alert.getUrl()); + } + + if (alert.getParam() != null) { + alertElement.setAttribute("param", alert.getParam()); + } + + if (alert.getOther() != null) { + alertElement.setAttribute("other", alert.getOther()); + } + + if (alert.getAttack() != null) { + alertElement.setAttribute("attack", alert.getAttack()); + } + + if (alert.getDescription() != null) { alertElement.setAttribute("description", alert.getDescription()); - if (alert.getSolution() != null) alertElement.setAttribute("solution", alert.getSolution()); - if (alert.getReference() != null) + } + + if (alert.getSolution() != null) { + alertElement.setAttribute("solution", alert.getSolution()); + } + + if (alert.getReference() != null) { alertElement.setAttribute("reference", alert.getReference()); - alertsFound.addContent(alertElement); + } + + alertsParent.appendChild(alertElement); } + /** + * Reads alerts of a given type from the file. + * + * @param file The XML file previously written by {@link #saveAlertsToFile}. + * @param alertType The wrapper element name under <alerts>: + *
    + *
  • "alertsFound" + *
  • "alertsNotFound" + *
  • "ignoredAlertsFound" + *
+ * + * @return list of {@link Alert}s found inside the matching wrapper(s). + */ public static List getAlertsFromFile(File file, String alertType) - throws JDOMException, IOException { + throws IOException { + List alerts = new ArrayList<>(); - SAXBuilder parser = new SAXBuilder(); - Document alertsDoc = parser.build(file); - @SuppressWarnings("unchecked") - List alertElements = alertsDoc.getRootElement().getChildren(alertType); - for (Element element : alertElements) { - String name = element.getAttributeValue("name"); - if (name == null) { - // TODO Remove once alert attribute is no longer supported. - name = element.getAttributeValue("alert"); + + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document alertsDoc = builder.parse(file); + + Element root = alertsDoc.getDocumentElement(); + + NodeList rootChildren = root.getChildNodes(); + for (int i = 0; i < rootChildren.getLength(); i++) { + Node wrapperNode = rootChildren.item(i); + if (wrapperNode.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + + Element wrapperElem = (Element) wrapperNode; + if (!alertType.equals(wrapperElem.getTagName())) { + continue; + } + + NodeList childNodes = wrapperElem.getChildNodes(); + for (int j = 0; j < childNodes.getLength(); j++) { + Node node = childNodes.item(j); + if (node.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + + Element element = (Element) node; + if (!"alert".equals(element.getTagName())) { + continue; + } + + String name = element.getAttribute("name"); + if (name == null || name.isEmpty()) { + // TODO Remove once alert attribute is no longer supported. + name = element.getAttribute("alert"); + } + + Alert alert = + new Alert( + emptyToNull(element.getAttribute("url")), + emptyToNull(element.getAttribute("risk")), + emptyToNull(element.getAttribute("confidence")), + emptyToNull(element.getAttribute("param")), + emptyToNull(element.getAttribute("other")), + name); + + alerts.add(alert); + } } - Alert alert = - new Alert( - name, - element.getAttributeValue("url"), - element.getAttributeValue("risk"), - element.getAttributeValue("confidence"), - element.getAttributeValue("param"), - element.getAttributeValue("other")); - alerts.add(alert); + + } catch (ParserConfigurationException | SAXException e) { + throw new RemoteException("Failed to read alerts from file: " + file, e); } + return alerts; } -} + + private static String emptyToNull(String value) { + return (value == null || value.isEmpty()) ? null : value; + } +} \ No newline at end of file diff --git a/subprojects/zap-clientapi/zap-clientapi.gradle b/subprojects/zap-clientapi/zap-clientapi.gradle index 8fa565e..9ce319b 100644 --- a/subprojects/zap-clientapi/zap-clientapi.gradle +++ b/subprojects/zap-clientapi/zap-clientapi.gradle @@ -9,9 +9,6 @@ sourceSets { examples } assemble.dependsOn examplesClasses dependencies { - // XXX Change to implementation (it's not exposed in public API) when bumping major version. - api 'org.jdom:jdom:1.1.3' - examplesImplementation sourceSets.main.output }