From b04b684a78fe57a1f1b08e51333179473aec53c2 Mon Sep 17 00:00:00 2001 From: apeiris Date: Mon, 25 Aug 2025 10:08:07 -0400 Subject: [PATCH 1/2] Add secure property and XML retrieval endpoints to ApiResource - Introduced /getPropertyList endpoint returning application properties as JSON. - Introduced /getXml endpoint returning XML content filtered via XPath. - Added HTTPS enforcement for non-localhost requests to prevent exposure of sensitive information. - Implemented isLocalhost(Request) helper for local testing exceptions. - Added XML parsing with XXE protection. - Updated imports and cleaned up unused imports. --- .../cmd/processor/restapi/ApiResource.java | 159 +++++++++++++++--- 1 file changed, 136 insertions(+), 23 deletions(-) diff --git a/Server/src/main/java/org/openas2/cmd/processor/restapi/ApiResource.java b/Server/src/main/java/org/openas2/cmd/processor/restapi/ApiResource.java index a5bf91a2..1d8b16d9 100644 --- a/Server/src/main/java/org/openas2/cmd/processor/restapi/ApiResource.java +++ b/Server/src/main/java/org/openas2/cmd/processor/restapi/ApiResource.java @@ -5,48 +5,69 @@ */ package org.openas2.cmd.processor.restapi; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import org.openas2.cert.AliasedCertificateFactory; -import org.openas2.cert.CertificateFactory; -import org.openas2.cmd.CommandResult; -import org.openas2.cmd.processor.RestCommandProcessor; import jakarta.annotation.security.RolesAllowed; import jakarta.ws.rs.Consumes; - import jakarta.ws.rs.DefaultValue; - - +import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; +import jakarta.ws.rs.HEAD; import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.PathParam; import jakarta.ws.rs.POST; +import jakarta.ws.rs.Produces; import jakarta.ws.rs.PUT; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.HEAD; - - -import jakarta.ws.rs.PathParam; - +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Context; - -import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.core.Request; -import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; + import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.StringWriter; import java.security.cert.Certificate; import java.security.cert.X509Certificate; + import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathFactory; + +import org.glassfish.grizzly.http.server.Request; + +import org.openas2.cert.AliasedCertificateFactory; +import org.openas2.cert.CertificateFactory; +import org.openas2.cmd.CommandResult; +import org.openas2.cmd.processor.RestCommandProcessor; +import org.openas2.Session; +import org.openas2.util.Properties; + import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + + + /** * @author javier */ @@ -70,12 +91,11 @@ public static void setProcessor(RestCommandProcessor aProcessor) { private static RestCommandProcessor processor; @Context UriInfo ui; - @Context Request request; private final ObjectMapper mapper; - + public ApiResource() { - + mapper = new ObjectMapper(); // enable pretty printing mapper.enable(SerializationFeature.INDENT_OUTPUT); @@ -220,6 +240,99 @@ public Response headCommand(@PathParam("param") String command) { return Response.status(200).build(); } + @GET + @RolesAllowed({"ADMIN"}) + @Path("/getPropertyList") + @Produces(MediaType.APPLICATION_JSON) + public Response getPropertyList(@Context Request request) { + if (!request.isSecure() && !isLocalhost(request)) { + return Response.status(Response.Status.FORBIDDEN) + .entity("{\"error\":\"SSL/TLS required\"}") + .type(MediaType.APPLICATION_JSON) + .build(); + } + Map result = new HashMap<>(); + try { + result = (Map) Properties.getProperties(); + }catch(Exception ex) { + LoggerFactory.getLogger(ApiResource.class.getName()).error(ex.getMessage(), ex); + throw ex; + } + ObjectMapper om = new ObjectMapper(); + try { + String js = om.writeValueAsString(result); + return Response.ok(js, MediaType.APPLICATION_JSON).build(); + } catch (JsonProcessingException e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("error").type(MediaType.APPLICATION_JSON).build(); + } + } + + @GET + @RolesAllowed({"ADMIN"}) + @Path("/getXml") + @Produces({MediaType.APPLICATION_XML,MediaType.APPLICATION_JSON}) + public Response getXml(@Context Request request, @QueryParam("filename") String filename, @QueryParam("xpath") String xpathExpression){ + + if (!request.isSecure() && !isLocalhost(request)) { + return Response.status(Response.Status.FORBIDDEN) + .entity("{\"error\":\"SSL/TLS required\"}") + .type(MediaType.APPLICATION_JSON) + .build(); + } + + Session session = getProcessor().getSession(); + String filePath = session.getBaseDirectory() + "/" + filename; + try { + NodeList nodeList = getNodes(filePath, xpathExpression); + DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document resultDocument = db.newDocument(); + for (int i = 0; i < nodeList.getLength(); i++) { + Node importedNode = resultDocument.importNode(nodeList.item(i), true); + resultDocument.appendChild(importedNode); + } + StringWriter stringWriter = new StringWriter(); // Convert the XML document to a string + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.transform(new DOMSource(resultDocument), new StreamResult(stringWriter)); + String xmlContent = stringWriter.toString(); + return Response.ok(xmlContent, MediaType.APPLICATION_XML).build(); + } catch (Exception exception) { + return Response.serverError().entity("error").type(MediaType.APPLICATION_JSON).build(); + } + } + + private static boolean isLocalhost(Request request) { + boolean isLocalhost = request.getRemoteAddr().equals("127.0.0.1") || request.getRemoteAddr().equals("::1"); + return isLocalhost; + } + + private NodeList getNodes(String xmlFileName, String xpathExpression) { + NodeList nodeList = null; + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + + // === XXE Protection === + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); + dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + dbf.setXIncludeAware(false); + dbf.setExpandEntityReferences(false); + + DocumentBuilder db = dbf.newDocumentBuilder(); + File file = new File(xmlFileName); + Document document = db.parse(file); + + XPathExpression xPathExpr = XPathFactory.newInstance().newXPath().compile(xpathExpression); + nodeList = (NodeList) xPathExpr.evaluate(document, XPathConstants.NODESET); + + } catch (Exception ex) { + LoggerFactory.getLogger(ApiResource.class.getName()).error("Error parsing XML file: " + xmlFileName, ex); + // return null on error + } + return nodeList; + } + private CommandResult importCertificateByStream(String itemId, MultivaluedMap formParams) throws Exception { try { List params = new ArrayList(); @@ -245,7 +358,7 @@ private CommandResult importCertificateByStream(String itemId, MultivaluedMap Date: Thu, 28 Aug 2025 13:10:09 -0400 Subject: [PATCH 2/2] Redact sensitive properties and XML attributes (password/pwd) in REST API responses --- .../cmd/processor/restapi/ApiResource.java | 86 +++++++++++++++++-- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/Server/src/main/java/org/openas2/cmd/processor/restapi/ApiResource.java b/Server/src/main/java/org/openas2/cmd/processor/restapi/ApiResource.java index 1d8b16d9..3d43d3be 100644 --- a/Server/src/main/java/org/openas2/cmd/processor/restapi/ApiResource.java +++ b/Server/src/main/java/org/openas2/cmd/processor/restapi/ApiResource.java @@ -37,6 +37,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -60,11 +61,13 @@ import org.openas2.Session; import org.openas2.util.Properties; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import org.w3c.dom.NamedNodeMap; @@ -254,7 +257,15 @@ public Response getPropertyList(@Context Request request) { Map result = new HashMap<>(); try { result = (Map) Properties.getProperties(); - }catch(Exception ex) { + for (String key : new HashSet<>(result.keySet())) { // === Redact sensitive entries === + String lowerKey = key.toLowerCase(); + if (lowerKey.contains("password") || lowerKey.contains("pwd")) { + result.remove(key); + // result.computeIfPresent(key, (k, v) -> "***REDACTED***"); // mask instead of removing + } + } + + } catch (Exception ex) { LoggerFactory.getLogger(ApiResource.class.getName()).error(ex.getMessage(), ex); throw ex; } @@ -263,7 +274,10 @@ public Response getPropertyList(@Context Request request) { String js = om.writeValueAsString(result); return Response.ok(js, MediaType.APPLICATION_JSON).build(); } catch (JsonProcessingException e) { - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("error").type(MediaType.APPLICATION_JSON).build(); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("{\"error\":\"Serialization failed\"}") + .type(MediaType.APPLICATION_JSON) + .build(); } } @@ -271,15 +285,16 @@ public Response getPropertyList(@Context Request request) { @RolesAllowed({"ADMIN"}) @Path("/getXml") @Produces({MediaType.APPLICATION_XML,MediaType.APPLICATION_JSON}) - public Response getXml(@Context Request request, @QueryParam("filename") String filename, @QueryParam("xpath") String xpathExpression){ - + public Response getXml(@Context Request request, + @QueryParam("filename") String filename, + @QueryParam("xpath") String xpathExpression) { + // Require HTTPS unless localhost if (!request.isSecure() && !isLocalhost(request)) { return Response.status(Response.Status.FORBIDDEN) .entity("{\"error\":\"SSL/TLS required\"}") .type(MediaType.APPLICATION_JSON) .build(); } - Session session = getProcessor().getSession(); String filePath = session.getBaseDirectory() + "/" + filename; try { @@ -288,16 +303,49 @@ public Response getXml(@Context Request request, @QueryParam("filename") String Document resultDocument = db.newDocument(); for (int i = 0; i < nodeList.getLength(); i++) { Node importedNode = resultDocument.importNode(nodeList.item(i), true); + redactSensitiveAttributes(importedNode); // === Redact sensitive attributes === resultDocument.appendChild(importedNode); } - StringWriter stringWriter = new StringWriter(); // Convert the XML document to a string + StringWriter stringWriter = new StringWriter();// Convert XML document to string TransformerFactory transformerFactory = TransformerFactory.newInstance(); Transformer transformer = transformerFactory.newTransformer(); transformer.transform(new DOMSource(resultDocument), new StreamResult(stringWriter)); + String xmlContent = stringWriter.toString(); return Response.ok(xmlContent, MediaType.APPLICATION_XML).build(); + } catch (Exception exception) { - return Response.serverError().entity("error").type(MediaType.APPLICATION_JSON).build(); + LoggerFactory.getLogger(ApiResource.class.getName()) + .error("Error building XML response", exception); + return Response.serverError() + .entity("{\"error\":\"Internal Server Error\"}") + .type(MediaType.APPLICATION_JSON) + .build(); + } + } + + /** + * Remove or mask sensitive attributes (password/pwd). + */ + private void redactSensitiveAttributes(Node node) { + if (node == null){ + return; + } + if (node.hasAttributes()) { + NamedNodeMap attrs = node.getAttributes(); + for (int j = attrs.getLength() - 1; j >= 0; j--) { + Node attr = attrs.item(j); + String attrName = attr.getNodeName().toLowerCase(); + if (attrName.contains("password") || attrName.contains("pwd")) { + // Either mask or remove + //attr.setNodeValue("***REDACTED***"); + attrs.removeNamedItem(attr.getNodeName()); + } + } + } + NodeList children = node.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + redactSensitiveAttributes(children.item(i)); } } @@ -325,14 +373,36 @@ private NodeList getNodes(String xmlFileName, String xpathExpression) { XPathExpression xPathExpr = XPathFactory.newInstance().newXPath().compile(xpathExpression); nodeList = (NodeList) xPathExpr.evaluate(document, XPathConstants.NODESET); + Logger Log=LoggerFactory.getLogger(ApiResource.class.getName()); + Log.info("nodelist="+nodeList.getLength());// ***************** + + if (nodeList != null) { + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + if (node.hasAttributes()) { + NamedNodeMap attrs = node.getAttributes(); + for (int j = attrs.getLength() - 1; j >= 0; j--) { + Node attr = attrs.item(j); + String attrName = attr.getNodeName().toLowerCase(); + if (attrName.contains("password") || attrName.contains("pwd")) { + attrs.removeNamedItem(attr.getNodeName()); // Remove the sensitive attribute + // Or mask instead: attr.setNodeValue("***REDACTED***"); + } + } + } + } + } } catch (Exception ex) { - LoggerFactory.getLogger(ApiResource.class.getName()).error("Error parsing XML file: " + xmlFileName, ex); + LoggerFactory.getLogger(ApiResource.class.getName()) + .error("Error parsing XML file: " + xmlFileName, ex); // return null on error } return nodeList; } + + private CommandResult importCertificateByStream(String itemId, MultivaluedMap formParams) throws Exception { try { List params = new ArrayList();