Skip to content

Commit

Permalink
chore(security) #28388 : XML External Entity (XXE) Injection in JDOM (#…
Browse files Browse the repository at this point in the history
…29006)

### Proposed Changes
* Removes the use of the JDOM library altogether.
* The classes using the JDOM library living under the
`dotCMS/src/main/java/org/apache/velocity/anakia/` and
`dotcms-integration/src/test/java/com/ettrema/` packages were removed
altogether as well.
* As per Steve Bolton's suggestion, we're now using the JAXB library to
handle XML data representing Portlets in dotCMS. This includes both the
Portlets defined in the `/WEB-INF/portlet.xml` and
`/WEB-INF/portlet-ext.xml` files, and the database.
* Additional Javadoc and code refactoring was done.
* The `dotcms-postman/src/main/resources/postman/PortletResource.json`
Postman suite was refactored to use JWT, organized into folders, and
reviewed t make sure that the REST Endpoint is tested as much as
possible.
  • Loading branch information
jcastro-dotcms committed Jun 26, 2024
1 parent cec226a commit 7f3a48f
Show file tree
Hide file tree
Showing 53 changed files with 1,222 additions and 5,939 deletions.
6 changes: 0 additions & 6 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1251,12 +1251,6 @@
<version>2.1.3</version>
</dependency>

<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom</artifactId>
<version>1.1.3</version>
</dependency>

<dependency>
<!-- XML Stream Processing like Sax -->
<groupId>xmlpull</groupId>
Expand Down
5 changes: 0 additions & 5 deletions dotCMS/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1069,11 +1069,6 @@
<artifactId>dom4j</artifactId>
</dependency>

<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom</artifactId>
</dependency>

<dependency>
<!-- XML Stream Processing like Sax -->
<groupId>xmlpull</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.dotcms.rest.api.v1.portlet;

import com.dotcms.exception.ExceptionUtil;
import com.dotcms.repackage.com.google.common.annotations.VisibleForTesting;
import com.dotcms.rest.InitDataObject;
import com.dotcms.rest.ResponseEntityView;
Expand All @@ -19,6 +20,7 @@
import com.dotmarketing.exception.DotDataException;
import com.dotmarketing.exception.DotSecurityException;
import com.dotmarketing.util.Logger;
import com.dotmarketing.util.PortletID;
import com.dotmarketing.util.UtilMethods;
import com.liferay.portal.model.Portlet;
import com.liferay.portal.model.User;
Expand Down Expand Up @@ -46,21 +48,24 @@
import java.util.stream.Collectors;

import static com.liferay.portal.model.Portlet.DATA_VIEW_MODE_KEY;
import static com.liferay.util.StringPool.BLANK;

/**
* This Resource is for create custom portlets. These kind of custom portlets are to show diff types
* or content (content types or base types).
*/
@Path("/v1/portlet")
@SuppressWarnings("serial")
public class PortletResource implements Serializable {

private final WebResource webResource;
private final PortletAPI portletApi;

private static final String JSON_RESPONSE_PORTLET_ATTR = "portlet";

/**
* Default class constructor.
*/
@SuppressWarnings("unused")
public PortletResource() {
this(new WebResource(new ApiProvider()), APILocator.getPortletAPI());
}
Expand All @@ -72,33 +77,35 @@ public PortletResource(WebResource webResource, PortletAPI portletApi) {
}

/**
* Creates a Portlet for a given name content-types and Display view
* @param request
* @param formData
* @return
* Creates a custom dotCMS Portlet for a given Base Type or Content Type.
*
* @param request The current instance of the {@link HttpServletRequest}.
* @param formData The {@link CustomPortletForm} containing the information for the new
* Portlet.
*
* @return A {@link Response} object with the ID of the new portlet.
*/
@POST
@Path("/custom")
@JSONP
@NoCache
@Consumes(MediaType.APPLICATION_JSON)
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
public final Response saveNew(@Context final HttpServletRequest request, final CustomPortletForm formData) {

public final Response saveNew(@Context final HttpServletRequest request,
final CustomPortletForm formData) {
final InitDataObject initData = new WebResource.InitBuilder(webResource)
.requiredBackendUser(true)
.requiredFrontendUser(false)
.requestAndResponse(request, null)
.rejectWhenNoUser(true)
.requiredPortlet("roles")
.requiredPortlet(PortletID.ROLES.toString())
.init();

Response response = null;

String portletId = BLANK;
try {
final String portletId = portletApi.portletIdPrefixCleaner(formData.portletId);
portletId = portletApi.portletIdPrefixCleaner(formData.portletId);
if (UtilMethods.isSet(portletApi.findPortlet(portletId))) {
throw new DoesNotExistException("Portlet with Id: " + formData.portletId + " already exist");
throw new DoesNotExistException(String.format("Portlet with ID '%s' already exist",
formData.portletId));
}

final Portlet contentPortlet = portletApi.findPortlet("content");
Expand All @@ -112,13 +119,12 @@ public final Response saveNew(@Context final HttpServletRequest request, final C
final Portlet newPortlet = APILocator.getPortletAPI()
.savePortlet(new DotPortlet(portletId, contentPortlet.getPortletClass(), initValues), initData.getUser());

return Response.ok(new ResponseEntityView(Map.of("portlet", newPortlet.getPortletId()))).build();

} catch (Exception e) {
response = ResponseUtil.mapExceptionResponse(e);
return Response.ok(new ResponseEntityView<>(Map.of(JSON_RESPONSE_PORTLET_ATTR, newPortlet.getPortletId()))).build();
} catch (final Exception e) {
Logger.error(this, String.format("An error occurred when saving new Portlet with ID " +
"'%s': %s", portletId, ExceptionUtil.getErrorMessage(e)), e);
return ResponseUtil.mapExceptionResponse(e);
}

return response;
}

/**
Expand Down Expand Up @@ -161,7 +167,7 @@ public final Response updatePortlet(@Context final HttpServletRequest request, f
final Portlet newPortlet = APILocator.getPortletAPI()
.savePortlet(new DotPortlet(portletId, contentPortlet.getPortletClass(), initValues), initData.getUser());

return Response.ok(new ResponseEntityView(Map.of("portlet", newPortlet.getPortletId()))).build();
return Response.ok(new ResponseEntityView<>(Map.of(JSON_RESPONSE_PORTLET_ATTR, newPortlet.getPortletId()))).build();

} catch (Exception e) {
response = ResponseUtil.mapExceptionResponse(e);
Expand Down Expand Up @@ -246,8 +252,8 @@ public final Response addContentPortletToLayout(@Context final HttpServletReques

layoutAPI.setPortletIdsToLayout(layout, portletIds);

return Response.ok(new ResponseEntityView(
Map.of("portlet", portlet.getPortletId(), "layout", layout.getId())))
return Response.ok(new ResponseEntityView<>(
Map.of(JSON_RESPONSE_PORTLET_ATTR, portlet.getPortletId(), "layout", layout.getId())))
.build();

}
Expand Down
183 changes: 180 additions & 3 deletions dotCMS/src/main/java/com/dotmarketing/business/portal/DotPortlet.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,192 @@
package com.dotmarketing.business.portal;

import java.util.Map;

import com.dotmarketing.util.UtilMethods;
import com.liferay.portal.model.Portlet;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* This class implements the JAXB mapping for the portlet definitions in dotCMS.
*
* @author Jose Castro
* @since Jun 17th, 2024
*/
@XmlRootElement(name = "portlet")
public class DotPortlet extends Portlet {

private static final long serialVersionUID = 1L;
@XmlElement(name = "init-param")
private List<InitParam> initParams;

/**
* Default class constructor, required by JAXB.
*/
@SuppressWarnings("unused")
public DotPortlet() {
super(null, null, new HashMap<>());
}

/**
* Creates an instance of this class based on the original/legacy definition or a dotCMS
* Portlet.
*
* @param portlet The original {@link Portlet} object.
*/
public DotPortlet(final Portlet portlet) {
this(portlet.getPortletId(), portlet.getPortletClass(), portlet.getInitParams());
}

/**
* Creates an instance of this class.
*
* @param portletId The ID of the portlet.
* @param portletClass The base class that handles the existing legacy Liferay/Struts logic to
* render the portlet.
* @param initParams The initialization and/or configuration parameters for the portlet.
*/
public DotPortlet(String portletId, String portletClass, Map<String, String> initParams) {
super(portletId, portletClass, initParams);
this.initParams = new ArrayList<>();
for (Map.Entry<String, String> entry : initParams.entrySet()) {
this.initParams.add(new InitParam(entry.getKey(), entry.getValue()));
}
}

@Override
@XmlElement(name = "portlet-name")
public String getPortletId() {
return portletId;
}

@Override
public void setPortletId(final String portletId) {
super.portletId = portletId;
}

@Override
@XmlElement(name = "portlet-class")
public String getPortletClass() {
return portletClass;
}

@Override
public void setPortletClass(final String portletClass) {
super.portletClass = portletClass;
}

@SuppressWarnings("unused")
@XmlElement(name = "init-param")
public List<InitParam> getInitParamList() {
return this.initParams;
}

/**
* Returns the initialization parameters of the portlet as a map. By default, the JAXB mapping
* will read them as a list of attributes, but, for backward compatibility, we need to return
* them as mapped values.
*
* @return A map with the initialization parameters of the portlet.
*/
@Override
public Map<String, String> getInitParams() {
if (UtilMethods.isSet(this.initParams)) {
for (final InitParam initParam : this.initParams) {
super.initParams.put(initParam.getName(), initParam.getValue());
}
}
return super.getInitParams();
}

@Override
public String toString() {
return "DotPortlet{" +
" portletId='" + this.portletId + '\'' +
", portletClass='" + this.portletClass + '\'' +
", portletSource='" + this.portletSource + '\'' +
", initParamsAsList=" + this.initParams +
'}';
}

/**
* Represents the initialization parameter tags in a Portlet definition. It can contain any
* value/pair required by the Portlet, there's no technical limitation.
*/
@XmlAccessorType(XmlAccessType.FIELD)
public static class InitParam {

@XmlElement(name = "name")
private String name;

@XmlElement(name = "value")
private String value;

/**
* Default class constructor, required by JAXB.
*/
@SuppressWarnings("unused")
public InitParam() {
}

/**
* Creates an instance of an initialization parameter.
*
* @param name The name of the parameter.
* @param value The value of the parameter.
*/
public InitParam(final String name, final String value) {
this.name = name;
this.value = value;
}

/**
* Returns the name of the parameter.
*
* @return The name of the parameter.
*/
public String getName() {
return this.name;
}

/**
* Sets the name of the parameter.
*
* @param name The name of the parameter.
*/
public void setName(String name) {
this.name = name;
}

/**
* Returns the value of the parameter.
*
* @return The value of the parameter.
*/
public String getValue() {
return this.value;
}

/**
* Sets the value of the parameter.
*
* @param value The value of the parameter.
*/
public void setValue(String value) {
this.value = value;
}

@Override
public String toString() {
return "InitParam{" +
"name='" + this.name + '\'' +
", value='" + this.value + '\'' +
'}';
}

}

Expand Down
Loading

0 comments on commit 7f3a48f

Please sign in to comment.