-
Notifications
You must be signed in to change notification settings - Fork 166
Feature/api secure endpoints #466
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b04b684
29635b1
b6f5404
3f9faaa
e3c224f
4054cfa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,3 +17,4 @@ pom.xml.versionsBackup | |
| **/.settings | ||
| **/*.class | ||
| .metadata/* | ||
| "Server/OPENAS2_LOG_DIR_IS_UNDEFINED/*.txt" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,48 +5,76 @@ | |
| */ | ||
| 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 java.util.Set; | ||
|
|
||
|
|
||
| import javax.xml.parsers.DocumentBuilder; | ||
| import javax.xml.parsers.DocumentBuilderFactory; | ||
| import javax.xml.transform.dom.DOMSource; | ||
| import javax.xml.transform.stream.StreamResult; | ||
| import javax.xml.transform.Transformer; | ||
| import javax.xml.transform.TransformerFactory; | ||
|
|
||
|
|
||
|
|
||
| import javax.xml.xpath.XPath; | ||
| 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.Element; | ||
| import org.w3c.dom.Node; | ||
| import org.w3c.dom.NodeList; | ||
|
|
||
|
|
||
|
|
||
| /** | ||
| * @author javier | ||
| */ | ||
|
|
@@ -70,12 +98,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 +247,139 @@ 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<String, String> result = new HashMap<>(); | ||
| try { | ||
| result = (Map<String, String>) 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(); | ||
| } | ||
|
|
||
|
|
||
| Set<String> allowedXPaths = Set.of( | ||
| "/partnerships/partner", | ||
| "/partnerships/partner/name", | ||
| "/partnerships/*", | ||
| "*" | ||
| ); | ||
|
|
||
| if (xpathExpression == null || !allowedXPaths.contains(xpathExpression)) { | ||
| return Response.status(Response.Status.BAD_REQUEST) | ||
|
|
||
| .entity("{\"error\":\"Invalid or disallowed XPath expression\"}") | ||
| .type(MediaType.APPLICATION_JSON) | ||
| .build(); | ||
| } | ||
|
|
||
| Session session = getProcessor().getSession(); | ||
| String filePath = session.getBaseDirectory() + "/" + filename; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since there is no validation of the file path this could retrieve any XML file on the server and is a security risk. EDIT: I was too quick off the mark on this comment - I see it is "protected" by enforcing HTTPS and can therefore be retrieved from anywhere in the world but it will still return sensitive information such as passwords if they are hard coded into the config.xml file and since you do not restrict which files this API endpoint can acces it can return the config.xml file. |
||
|
|
||
| try { | ||
|
|
||
| DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); | ||
| factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); // prevent XXE | ||
| DocumentBuilder builder = factory.newDocumentBuilder(); | ||
| Document document = builder.parse(new File(filePath)); | ||
|
|
||
|
|
||
| XPath xpath = XPathFactory.newInstance().newXPath(); | ||
| XPathExpression expr = xpath.compile(xpathExpression); | ||
| NodeList nodeList = (NodeList) expr.evaluate(document, XPathConstants.NODESET); | ||
|
|
||
|
|
||
| Document resultDocument = builder.newDocument(); | ||
| Element root = resultDocument.createElement("results"); | ||
| resultDocument.appendChild(root); | ||
|
|
||
| for (int i = 0; i < nodeList.getLength(); i++) { | ||
| Node importedNode = resultDocument.importNode(nodeList.item(i), true); | ||
| root.appendChild(importedNode); | ||
| } | ||
|
|
||
| // Convert to string | ||
| Transformer transformer = TransformerFactory.newInstance().newTransformer(); | ||
| StringWriter stringWriter = new StringWriter(); | ||
| transformer.transform(new DOMSource(resultDocument), new StreamResult(stringWriter)); | ||
|
|
||
| return Response.ok(stringWriter.toString(), MediaType.APPLICATION_XML).build(); | ||
|
|
||
| } catch (Exception exception) { | ||
| LoggerFactory.getLogger(ApiResource.class.getName()).error( | ||
| "Error processing XML file: " + filePath, exception); | ||
| return Response.serverError() | ||
| .entity("{\"error\":\"Internal server 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; | ||
| } | ||
|
Comment on lines
+356
to
+381
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This appears to be a redundant method.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The XML read here is used specifically to retrieve partnership endpoint information (polling location).
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is not called from anywhere so I am unclear why it is here unless you have other code that is not included in this PR. |
||
|
|
||
| private CommandResult importCertificateByStream(String itemId, MultivaluedMap<String, String> formParams) throws Exception { | ||
| try { | ||
| List<String> params = new ArrayList<String>(); | ||
|
|
@@ -245,7 +405,7 @@ private CommandResult importCertificateByStream(String itemId, MultivaluedMap<St | |
| } catch (Exception ex) { | ||
| LoggerFactory.getLogger(ApiResource.class.getName()).error(ex.getMessage(), ex); | ||
| throw ex; | ||
| // return Response.status(506).entity( ex.getMessage()).build(); | ||
|
|
||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this returns the parsed properties not the properties as set in the config.xml proerties section or the properties file.
If any properties use property references or envoronment variable references within their value then those references will have been replaced with actual values.
Based off the comment in your previous PR, you are using this for administration and debugging.
Are you intending to use this in a UI that allows you to change properties?
There are extensive debug logging statements that support figuring out issuses with properties if you set TRACE level logging.
Note also that there are a number of passwords in the properties list and passwords should not be returned in the list as it is a security risk. You will have to filter out password properties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, intended usage is primarily for administration and debugging. Regarding the passwords, would redacting properties before returning the properties address the concern? I can implement that to ensure sensitive information is not exposed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes - if you remove any property that has the word "password" in it's property name or property value that should work.
These are the currently known properties that have password information: