diff --git a/Dockerfile.tomcat b/Dockerfile.tomcat
index e928f65a1f..806b42538a 100644
--- a/Dockerfile.tomcat
+++ b/Dockerfile.tomcat
@@ -1,4 +1,4 @@
-FROM registry.hub.docker.com/entando/entando-tomcat-base:7.5.0
+FROM registry.hub.docker.com/entando/entando-tomcat-base:v7.5.1-ESB-1170-PR-37-BB-release-2F-7.5
ARG VERSION
### Required OpenShift Labels
diff --git a/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/enumeratorAttribute.jsp b/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/enumeratorAttribute.jsp
index 3be99dec90..4e3af19cd7 100644
--- a/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/enumeratorAttribute.jsp
+++ b/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/enumeratorAttribute.jsp
@@ -1,10 +1,10 @@
<%@ taglib prefix="s" uri="/struts-tags" %>
-
+
-
+
diff --git a/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/enumeratorMapAttribute.jsp b/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/enumeratorMapAttribute.jsp
index 0033664720..75d0facaeb 100644
--- a/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/enumeratorMapAttribute.jsp
+++ b/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/enumeratorMapAttribute.jsp
@@ -2,16 +2,16 @@
–
- :
- :
+ :
+ :
–
- :
- :
+ :
+ :
diff --git a/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/hypertextAttribute.jsp b/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/hypertextAttribute.jsp
index 394fe9f80a..549bcd3623 100644
--- a/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/hypertextAttribute.jsp
+++ b/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/hypertextAttribute.jsp
@@ -1,2 +1,2 @@
<%@ taglib prefix="s" uri="/struts-tags" %>
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/longtextAttribute.jsp b/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/longtextAttribute.jsp
index ee76188e58..e6617303ad 100644
--- a/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/longtextAttribute.jsp
+++ b/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/longtextAttribute.jsp
@@ -1,2 +1,2 @@
<%@ taglib prefix="s" uri="/struts-tags" %>
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/textAttribute.jsp b/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/textAttribute.jsp
index ee76188e58..e6617303ad 100644
--- a/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/textAttribute.jsp
+++ b/admin-console/src/main/webapp/WEB-INF/apsadmin/jsp/entity/view/textAttribute.jsp
@@ -1,2 +1,2 @@
<%@ taglib prefix="s" uri="/struts-tags" %>
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/admin-console/src/main/webapp/error.jsp b/admin-console/src/main/webapp/error.jsp
index 8bec65d6fd..8074cf17f7 100644
--- a/admin-console/src/main/webapp/error.jsp
+++ b/admin-console/src/main/webapp/error.jsp
@@ -5,9 +5,9 @@
<%@ taglib prefix="wp" uri="/aps-core" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
- Object statusCode = request.getAttribute("javax.servlet.error.status_code");
- Object exceptionType = request.getAttribute("javax.servlet.error.exception_type");
- Object message = request.getAttribute("javax.servlet.error.message");
+ Object statusCode = request.getAttribute("jakarta.servlet.error.status_code");
+ Object exceptionType = request.getAttribute("jakarta.servlet.error.exception_type");
+ Object message = request.getAttribute("jakarta.servlet.error.message");
%>
diff --git a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/ContentManager.java b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/ContentManager.java
index 1ec8ee3738..b8454945aa 100644
--- a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/ContentManager.java
+++ b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/content/ContentManager.java
@@ -45,6 +45,7 @@
import org.entando.entando.ent.exception.EntRuntimeException;
import org.entando.entando.ent.util.EntLogging.EntLogFactory;
import org.entando.entando.ent.util.EntLogging.EntLogger;
+import org.entando.entando.ent.util.LabelSanitizer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
@@ -293,6 +294,7 @@ public String addContent(Content content) throws EntException {
private String addUpdateContent(Content content, boolean updateDate) throws EntException {
String id = null;
try {
+ content.setDescription(LabelSanitizer.stripMarkup(content.getDescription()));
content.setLastModified(new Date());
if (updateDate) {
content.incrementVersion(false);
diff --git a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/contentmodel/ContentModelManager.java b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/contentmodel/ContentModelManager.java
index 91dddcda8b..a91ed57f0c 100644
--- a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/contentmodel/ContentModelManager.java
+++ b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/contentmodel/ContentModelManager.java
@@ -33,6 +33,8 @@
import java.util.List;
import java.util.Map;
import java.util.Properties;
+
+import org.entando.entando.ent.util.LabelSanitizer;
import org.entando.entando.plugins.jacms.aps.system.services.content.widget.RowContentListHelper;
import org.entando.entando.ent.util.EntLogging.EntLogger;
import org.entando.entando.ent.util.EntLogging.EntLogFactory;
@@ -113,6 +115,7 @@ public void releaseTenantAware() {
*/
@Override
public void addContentModel(ContentModel model) throws EntException {
+ model.setDescription(LabelSanitizer.stripMarkup(model.getDescription()));
try {
this.getContentModelDAO().addContentModel(model);
this.getCacheWrapper().addContentModel(model);
@@ -149,6 +152,7 @@ public void removeContentModel(ContentModel model) throws EntException {
*/
@Override
public void updateContentModel(ContentModel model) throws EntException {
+ model.setDescription(LabelSanitizer.stripMarkup(model.getDescription()));
try {
this.getContentModelDAO().updateContentModel(model);
this.getCacheWrapper().updateContentModel(model);
diff --git a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/resource/ResourceManager.java b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/resource/ResourceManager.java
index 4b0d5ee5ad..7184c476a7 100644
--- a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/resource/ResourceManager.java
+++ b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/aps/system/services/resource/ResourceManager.java
@@ -40,6 +40,7 @@
import org.entando.entando.ent.util.EntLogging.EntLogger;
import org.entando.entando.ent.util.EntLogging.EntLogFactory;
import org.entando.entando.ent.util.EntSafeXmlUtils;
+import org.entando.entando.ent.util.LabelSanitizer;
import org.xml.sax.InputSource;
import jakarta.xml.bind.JAXBContext;
@@ -244,6 +245,7 @@ public void deleteResources(List resources) throws EntExcepti
*/
@Override
public void addResource(ResourceInterface resource) throws EntException {
+ resource.setDescription(LabelSanitizer.stripMarkup(resource.getDescription()));
try {
this.generateAndSetResourceId(resource, resource.getId());
this.getResourceDAO().addResource(resource);
@@ -268,7 +270,7 @@ public void updateResource(ResourceDataBean bean) throws EntException {
ResourceInterface oldResource = this.loadResource(bean.getResourceId());
try {
if (null == bean.getInputStream()) {
- oldResource.setDescription(bean.getDescr());
+ oldResource.setDescription(LabelSanitizer.stripMarkup(bean.getDescr()));
oldResource.setCategories(bean.getCategories());
oldResource.setMetadata(bean.getMetadata());
oldResource.setMainGroup(bean.getMainGroup());
@@ -299,6 +301,7 @@ public void updateResource(ResourceDataBean bean) throws EntException {
*/
@Override
public void updateResource(ResourceInterface resource) throws EntException {
+ resource.setDescription(LabelSanitizer.stripMarkup(resource.getDescription()));
try {
this.getResourceDAO().updateResource(resource);
this.notifyResourceChanging(resource, ResourceChangedEvent.UPDATE_OPERATION_CODE);
@@ -310,7 +313,7 @@ public void updateResource(ResourceInterface resource) throws EntException {
protected ResourceInterface createResource(ResourceDataBean bean) throws EntException {
ResourceInterface resource = this.createResourceType(bean.getResourceType());
- resource.setDescription(bean.getDescr());
+ resource.setDescription(LabelSanitizer.stripMarkup(bean.getDescr()));
resource.setMainGroup(bean.getMainGroup());
resource.setCategories(bean.getCategories());
resource.setMasterFileName(bean.getFileName());
diff --git a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/apsadmin/content/helper/ContentActionHelper.java b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/apsadmin/content/helper/ContentActionHelper.java
index f86eed98c6..5f22841753 100644
--- a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/apsadmin/content/helper/ContentActionHelper.java
+++ b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/apsadmin/content/helper/ContentActionHelper.java
@@ -23,6 +23,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.struts2.ServletActionContext;
import org.entando.entando.aps.system.services.actionlog.model.ActivityStreamInfo;
+import org.entando.entando.ent.util.LabelSanitizer;
import org.entando.entando.plugins.jacms.aps.system.services.content.helper.IContentHelper;
import org.entando.entando.plugins.jacms.aps.util.CmsPageUtil;
import org.entando.entando.ent.util.EntLogging.EntLogger;
@@ -134,7 +135,8 @@ public void scanEntity(IApsEntity entity, ActionSupport action) {
String[] args = {String.valueOf(maxLength)};
action.addFieldError(DESCR, action.getText("error.content.descr.wrongMaxLength", args));
}
- if (!descr.matches("([^\"])+")) {
+ if (!descr.matches("[^\"<>]+")) {
+ content.setDescription(LabelSanitizer.stripMarkup(content.getDescription()));
action.addFieldError(DESCR, action.getText("error.content.descr.wrongCharacters"));
}
}
diff --git a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/apsadmin/resource/AbstractResourceAction.java b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/apsadmin/resource/AbstractResourceAction.java
index 5507df1917..9c39f5d174 100644
--- a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/apsadmin/resource/AbstractResourceAction.java
+++ b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/apsadmin/resource/AbstractResourceAction.java
@@ -28,7 +28,7 @@
* @author E.Santoboni
*/
public abstract class AbstractResourceAction extends AbstractTreeAction {
-
+
public List getResourceTypeCodes() {
return this.getResourceManager().getResourceTypeCodes();
}
@@ -58,7 +58,14 @@ public ResourceInterface loadResource(String resourceId) throws Throwable {
public String getResourceTypeCode() {
return _resourceTypeCode;
}
+
+ // Accept only registered type codes; neutralizes reflected input at the single bind point (SSTI, WSTG-INPV-18).
public void setResourceTypeCode(String resourceTypeCode) {
+ if (null != resourceTypeCode && null != this.getResourceManager()
+ && !this.getResourceManager().getResourceTypeCodes().contains(resourceTypeCode)) {
+ this._resourceTypeCode = null;
+ return;
+ }
this._resourceTypeCode = resourceTypeCode;
}
diff --git a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/apsadmin/resource/MultipleResourceAction.java b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/apsadmin/resource/MultipleResourceAction.java
index e2ccbdeee8..a87dda240a 100644
--- a/cms-plugin/src/main/java/com/agiletec/plugins/jacms/apsadmin/resource/MultipleResourceAction.java
+++ b/cms-plugin/src/main/java/com/agiletec/plugins/jacms/apsadmin/resource/MultipleResourceAction.java
@@ -47,6 +47,12 @@ public class MultipleResourceAction extends ResourceAction {
public void validate() {
logger.debug("MultipleResourceAction validate");
savedId.clear();
+ // Reject unregistered type codes: blocks save() on unknown type and reflected sinks (SSTI, WSTG-INPV-18).
+ if (!this.getResourceManager().getResourceTypeCodes().contains(this.getResourceTypeCode())) {
+ logger.warn("Rejected unknown resourceTypeCode value");
+ this.addFieldError("resourceTypeCode", this.getText("error.resource.file.genericError"));
+ return;
+ }
if (ApsAdminSystemConstants.EDIT == this.getStrutsAction()) {
this.fetchFileDescriptions();
addFieldErrors(validateFileDescriptions());
diff --git a/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/content/contentDetails.jsp b/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/content/contentDetails.jsp
index 4a66793978..ba3692bf19 100644
--- a/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/content/contentDetails.jsp
+++ b/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/content/contentDetails.jsp
@@ -328,7 +328,7 @@
-
+
diff --git a/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/content/view/imageAttribute.jsp b/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/content/view/imageAttribute.jsp
index 8066909fd7..fd303a5a11 100644
--- a/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/content/view/imageAttribute.jsp
+++ b/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/content/view/imageAttribute.jsp
@@ -9,7 +9,7 @@
"
- alt="" />
+ alt="" escapeHtml="true" />
">
@@ -27,7 +27,7 @@
-
" alt="" />
+
" alt="" />
"> :
@@ -36,7 +36,7 @@
"
- alt="" />
+ alt="" />
">
diff --git a/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/content/view/linkAttribute.jsp b/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/content/view/linkAttribute.jsp
index 15362fb869..a8ba8ce976 100644
--- a/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/content/view/linkAttribute.jsp
+++ b/cms-plugin/src/main/webapp/WEB-INF/plugins/jacms/apsadmin/jsp/content/view/linkAttribute.jsp
@@ -42,10 +42,10 @@
<%-- link icon --%>
- " class="" title="">
+ " class="" title="">
<%-- text of the link --%>
-
+
diff --git a/cms-plugin/src/test/java/com/agiletec/plugins/jacms/apsadmin/resource/TestMultipleResourceAction.java b/cms-plugin/src/test/java/com/agiletec/plugins/jacms/apsadmin/resource/TestMultipleResourceAction.java
index f49b5cdb96..434739cdca 100644
--- a/cms-plugin/src/test/java/com/agiletec/plugins/jacms/apsadmin/resource/TestMultipleResourceAction.java
+++ b/cms-plugin/src/test/java/com/agiletec/plugins/jacms/apsadmin/resource/TestMultipleResourceAction.java
@@ -92,8 +92,8 @@ void testSaveNewResourceStrutsValidation() throws Throwable {
assertEquals(Action.INPUT, result);
ActionSupport action = this.getAction();
Map> actionFieldErrors = action.getFieldErrors();
- assertEquals(6, actionFieldErrors.size());
- assertEquals(1, actionFieldErrors.get("resourceTypeCode").size());
+ assertEquals(2, actionFieldErrors.size());
+ assertEquals(2, actionFieldErrors.get("resourceTypeCode").size());
assertEquals(1, actionFieldErrors.get("mainGroup").size());
}
@@ -131,8 +131,8 @@ void testSaveNewResource_2() throws Throwable {
assertEquals(Action.INPUT, result);
ActionSupport action = this.getAction();
Map> actionFieldErrors = action.getFieldErrors();
- assertEquals(5, actionFieldErrors.size());
- assertEquals(1, actionFieldErrors.get("resourceTypeCode").size());
+ assertEquals(1, actionFieldErrors.size());
+ assertEquals(2, actionFieldErrors.get("resourceTypeCode").size());
}
diff --git a/engine/src/main/java/com/agiletec/aps/system/common/entity/ApsEntityManager.java b/engine/src/main/java/com/agiletec/aps/system/common/entity/ApsEntityManager.java
index bd56e904e0..970cba53fd 100644
--- a/engine/src/main/java/com/agiletec/aps/system/common/entity/ApsEntityManager.java
+++ b/engine/src/main/java/com/agiletec/aps/system/common/entity/ApsEntityManager.java
@@ -59,6 +59,7 @@
import org.apache.commons.beanutils.BeanComparator;
import org.entando.entando.ent.util.EntLogging.EntLogger;
import org.entando.entando.ent.util.EntLogging.EntLogFactory;
+import org.entando.entando.ent.util.LabelSanitizer;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
@@ -254,6 +255,7 @@ public void addEntityPrototype(IApsEntity entityType) throws EntException {
if (null == entityType) {
throw new EntException("Invalid entity type to add");
}
+ this.sanitizeEntityTypeLabels(entityType);
Map newEntityTypes = this.getEntityTypes();
newEntityTypes.put(entityType.getTypeCode(), entityType);
this.updateEntityPrototypes(newEntityTypes);
@@ -271,6 +273,7 @@ public void updateEntityPrototype(IApsEntity entityType) throws EntException {
if (null == entityType) {
throw new EntException("Invalid entity type to update");
}
+ this.sanitizeEntityTypeLabels(entityType);
Map entityTypes = this.getEntityTypes();
IApsEntity oldEntityType = entityTypes.get(entityType.getTypeCode());
if (null == oldEntityType) {
@@ -282,6 +285,23 @@ public void updateEntityPrototype(IApsEntity entityType) throws EntException {
this.notifyEntityTypesChanging(oldEntityType, entityType, EntityTypesChangingEvent.UPDATE_OPERATION_CODE);
}
+ /**
+ * Strips markup from the user-supplied label fields of an entity type (its description
+ * and the name/description of each attribute) before it is persisted
+ */
+ private void sanitizeEntityTypeLabels(IApsEntity entityType) {
+ entityType.setTypeCode(LabelSanitizer.stripMarkup(entityType.getTypeCode()));
+ entityType.setTypeDescription(LabelSanitizer.stripMarkup(entityType.getTypeDescription()));
+ List attributes = entityType.getAttributeList();
+ if (null != attributes) {
+ for (AttributeInterface attribute : attributes) {
+ attribute.setName(LabelSanitizer.stripMarkup(attribute.getName()));
+ attribute.setDescription(LabelSanitizer.stripMarkup(attribute.getDescription()));
+ LabelSanitizer.stripMarkup(attribute.getNames());
+ }
+ }
+ }
+
protected void verifyReloadingNeeded(IApsEntity oldEntityType, IApsEntity newEntityType) {
if (this.getStatus(newEntityType.getTypeCode()) == STATUS_NEED_TO_RELOAD_REFERENCES) {
return;
diff --git a/engine/src/main/java/com/agiletec/aps/system/services/category/CategoryManager.java b/engine/src/main/java/com/agiletec/aps/system/services/category/CategoryManager.java
index 393bf7ddbd..0967cee118 100644
--- a/engine/src/main/java/com/agiletec/aps/system/services/category/CategoryManager.java
+++ b/engine/src/main/java/com/agiletec/aps/system/services/category/CategoryManager.java
@@ -23,6 +23,7 @@
import org.entando.entando.aps.system.services.tenants.RefreshableBeanTenantAware;
import org.entando.entando.ent.util.EntLogging.EntLogger;
import org.entando.entando.ent.util.EntLogging.EntLogFactory;
+import org.entando.entando.ent.util.LabelSanitizer;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
@@ -82,6 +83,8 @@ private void initCache() throws EntException {
*/
@Override
public void addCategory(Category category) throws EntException {
+ category.setCode(LabelSanitizer.stripMarkup(category.getCode()));
+ LabelSanitizer.stripMarkup(category.getTitles());
try {
this.getCategoryDAO().addCategory(category);
this.getCacheWrapper().addCategory(category);
@@ -121,6 +124,7 @@ public void deleteCategory(String code) throws EntException {
*/
@Override
public void updateCategory(Category category) throws EntException {
+ LabelSanitizer.stripMarkup(category.getTitles());
try {
this.getCategoryDAO().updateCategory(category);
this.getCacheWrapper().updateCategory(category);
diff --git a/engine/src/main/java/com/agiletec/aps/system/services/group/GroupManager.java b/engine/src/main/java/com/agiletec/aps/system/services/group/GroupManager.java
index e77b7aa0d6..565e763929 100644
--- a/engine/src/main/java/com/agiletec/aps/system/services/group/GroupManager.java
+++ b/engine/src/main/java/com/agiletec/aps/system/services/group/GroupManager.java
@@ -27,6 +27,7 @@
import org.apache.commons.beanutils.BeanComparator;
import org.entando.entando.ent.util.EntLogging.EntLogger;
import org.entando.entando.ent.util.EntLogging.EntLogFactory;
+import org.entando.entando.ent.util.LabelSanitizer;
/**
* Servizio gestore dei gruppi.
@@ -87,6 +88,7 @@ public void releaseTenantAware() {
@Override
public void addGroup(Group group) throws EntException {
try {
+ group.setDescr(LabelSanitizer.stripMarkup(group.getDescr()));
this.getGroupDAO().addGroup(group);
this.getCacheWrapper().addGroup(group);
} catch (Throwable t) {
@@ -121,6 +123,7 @@ public void removeGroup(Group group) throws EntException {
@Override
public void updateGroup(Group group) throws EntException {
try {
+ group.setDescr(LabelSanitizer.stripMarkup(group.getDescr()));
this.getGroupDAO().updateGroup(group);
this.getCacheWrapper().updateGroup(group);
} catch (Throwable t) {
diff --git a/engine/src/main/java/com/agiletec/aps/system/services/page/PageManager.java b/engine/src/main/java/com/agiletec/aps/system/services/page/PageManager.java
index b1b0337c7d..b934f21318 100644
--- a/engine/src/main/java/com/agiletec/aps/system/services/page/PageManager.java
+++ b/engine/src/main/java/com/agiletec/aps/system/services/page/PageManager.java
@@ -45,6 +45,7 @@
import org.entando.entando.ent.exception.EntException;
import org.entando.entando.ent.util.EntLogging.EntLogFactory;
import org.entando.entando.ent.util.EntLogging.EntLogger;
+import org.entando.entando.ent.util.LabelSanitizer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -131,6 +132,7 @@ public synchronized void deletePage(String pageCode) throws EntException {
*/
@Override
public synchronized void addPage(IPage page) throws EntException {
+ LabelSanitizer.stripMarkup(page.getTitles());
try {
IPage parent = this.getDraftPage(page.getParentCode());
if (null == parent) {
@@ -165,6 +167,7 @@ public synchronized void addPage(IPage page) throws EntException {
*/
@Override
public synchronized void updatePage(IPage page) throws EntException {
+ LabelSanitizer.stripMarkup(page.getTitles());
try {
this.getPageDAO().updatePage(page);
this.getCacheWrapper().updateDraftPage(page);
diff --git a/engine/src/main/java/com/agiletec/aps/system/services/pagemodel/PageModelDOM.java b/engine/src/main/java/com/agiletec/aps/system/services/pagemodel/PageModelDOM.java
index 75c661f356..66835e1fe6 100644
--- a/engine/src/main/java/com/agiletec/aps/system/services/pagemodel/PageModelDOM.java
+++ b/engine/src/main/java/com/agiletec/aps/system/services/pagemodel/PageModelDOM.java
@@ -109,6 +109,10 @@ private Element buildSketchXML(Frame frame) {
private void decodeDOM(String xmlText) throws EntException {
SAXBuilder builder = new SAXBuilder();
builder.setValidation(false);
+ builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ builder.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ builder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
StringReader reader = new StringReader(xmlText);
try {
_doc = builder.build(reader);
diff --git a/engine/src/main/java/com/agiletec/aps/system/services/pagemodel/PageModelManager.java b/engine/src/main/java/com/agiletec/aps/system/services/pagemodel/PageModelManager.java
index d61333449d..ba99896b5c 100644
--- a/engine/src/main/java/com/agiletec/aps/system/services/pagemodel/PageModelManager.java
+++ b/engine/src/main/java/com/agiletec/aps/system/services/pagemodel/PageModelManager.java
@@ -23,6 +23,7 @@
import org.entando.entando.aps.system.services.guifragment.GuiFragmentUtilizer;
import org.entando.entando.ent.util.EntLogging.EntLogFactory;
import org.entando.entando.ent.util.EntLogging.EntLogger;
+import org.entando.entando.ent.util.LabelSanitizer;
import java.util.*;
import java.util.regex.*;
@@ -87,6 +88,8 @@ public void addPageModel(PageModel pageModel) throws EntException {
logger.debug("Null page template can be add");
return;
}
+ pageModel.setCode(LabelSanitizer.stripMarkup(pageModel.getCode()));
+ pageModel.setDescription(LabelSanitizer.stripMarkup(pageModel.getDescription()));
try {
this.getPageModelDAO().addModel(pageModel);
this.getCacheWrapper().addPageModel(pageModel);
@@ -103,6 +106,8 @@ public void updatePageModel(PageModel pageModel) throws EntException {
logger.debug("Null page template can be update");
return;
}
+ pageModel.setCode(LabelSanitizer.stripMarkup(pageModel.getCode()));
+ pageModel.setDescription(LabelSanitizer.stripMarkup(pageModel.getDescription()));
try {
PageModel pageModelToUpdate = this.getCacheWrapper().getPageModel(pageModel.getCode());
if (null == pageModelToUpdate) {
diff --git a/engine/src/main/java/com/agiletec/aps/system/services/role/RoleManager.java b/engine/src/main/java/com/agiletec/aps/system/services/role/RoleManager.java
index 614d622fd9..8a3c0a592f 100644
--- a/engine/src/main/java/com/agiletec/aps/system/services/role/RoleManager.java
+++ b/engine/src/main/java/com/agiletec/aps/system/services/role/RoleManager.java
@@ -25,6 +25,7 @@
import com.agiletec.aps.system.services.role.cache.IRoleCacheWrapper;
import org.entando.entando.ent.util.EntLogging.EntLogger;
import org.entando.entando.ent.util.EntLogging.EntLogFactory;
+import org.entando.entando.ent.util.LabelSanitizer;
/**
* Servizio di gestione dei ruoli.
@@ -110,6 +111,7 @@ public void removeRole(Role role) throws EntException {
*/
@Override
public void updateRole(Role role) throws EntException {
+ role.setDescription(LabelSanitizer.stripMarkup(role.getDescription()));
try {
this.getRoleDAO().updateRole(role);
this.getRoleCacheWrapper().updateRole(role);
@@ -127,6 +129,7 @@ public void updateRole(Role role) throws EntException {
*/
@Override
public void addRole(Role role) throws EntException {
+ role.setDescription(LabelSanitizer.stripMarkup(role.getDescription()));
try {
this.getRoleDAO().addRole(role);
this.getRoleCacheWrapper().addRole(role);
diff --git a/engine/src/main/java/org/entando/entando/aps/system/services/guifragment/GuiFragmentManager.java b/engine/src/main/java/org/entando/entando/aps/system/services/guifragment/GuiFragmentManager.java
index d8ed2609c8..88c6895997 100644
--- a/engine/src/main/java/org/entando/entando/aps/system/services/guifragment/GuiFragmentManager.java
+++ b/engine/src/main/java/org/entando/entando/aps/system/services/guifragment/GuiFragmentManager.java
@@ -32,6 +32,7 @@
import org.entando.entando.aps.system.services.guifragment.event.GuiFragmentChangedEvent;
import org.entando.entando.ent.util.EntLogging.EntLogger;
import org.entando.entando.ent.util.EntLogging.EntLogFactory;
+import org.entando.entando.ent.util.LabelSanitizer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cache.annotation.CacheEvict;
@@ -128,6 +129,7 @@ protected FieldSearchFilter[] addFilter(FieldSearchFilter[] filters, FieldSearch
@CacheEvict(value = ICacheInfoManager.DEFAULT_CACHE_NAME, key = "'GuiFragment_'.concat(#guiFragment.code)")
public void addGuiFragment(GuiFragment guiFragment) throws EntException {
try {
+ guiFragment.setCode(LabelSanitizer.stripMarkup(guiFragment.getCode()));
this.getGuiFragmentDAO().insertGuiFragment(guiFragment);
this.notifyGuiFragmentChangedEvent(guiFragment, GuiFragmentChangedEvent.INSERT_OPERATION_CODE);
this.evictGroups();
@@ -141,6 +143,7 @@ public void addGuiFragment(GuiFragment guiFragment) throws EntException {
@CacheEvict(value = ICacheInfoManager.DEFAULT_CACHE_NAME, key = "'GuiFragment_'.concat(#guiFragment.code)")
public void updateGuiFragment(GuiFragment guiFragment) throws EntException {
try {
+ guiFragment.setCode(LabelSanitizer.stripMarkup(guiFragment.getCode()));
this.getGuiFragmentDAO().updateGuiFragment(guiFragment);
this.notifyGuiFragmentChangedEvent(guiFragment, GuiFragmentChangedEvent.UPDATE_OPERATION_CODE);
this.evictGroups();
diff --git a/engine/src/main/java/org/entando/entando/aps/system/services/widgettype/WidgetTypeManager.java b/engine/src/main/java/org/entando/entando/aps/system/services/widgettype/WidgetTypeManager.java
index 13b7aa5451..4229570b2a 100644
--- a/engine/src/main/java/org/entando/entando/aps/system/services/widgettype/WidgetTypeManager.java
+++ b/engine/src/main/java/org/entando/entando/aps/system/services/widgettype/WidgetTypeManager.java
@@ -30,6 +30,7 @@
import org.entando.entando.ent.exception.EntException;
import org.entando.entando.ent.util.EntLogging.EntLogFactory;
import org.entando.entando.ent.util.EntLogging.EntLogger;
+import org.entando.entando.ent.util.LabelSanitizer;
import java.util.ArrayList;
import java.util.Collections;
@@ -120,6 +121,7 @@ public void addWidgetType(WidgetType widgetType) throws EntException {
if (null != widgetType.getTypeParameters() && null != widgetType.getConfig()) {
throw new EntException("ERROR : Params not null and config not null");
}
+ LabelSanitizer.stripMarkup(widgetType.getTitles());
this.getWidgetTypeDAO().addWidgetType(widgetType);
this.getCacheWrapper().addWidgetType(widgetType);
this.notifyWidgetTypeChanging(widgetType.getCode(), WidgetTypeChangedEvent.INSERT_OPERATION_CODE);
@@ -220,6 +222,7 @@ public void updateWidgetType(String widgetTypeCode, ApsProperties titles, ApsPro
} else {
readonlyPageWidgetConfigLocalVar = readonlyPageWidgetConfig;
}
+ LabelSanitizer.stripMarkup(titles);
this.getWidgetTypeDAO().updateWidgetType(widgetTypeCode, titles, defaultConfig, mainGroup, configUi, bundleId, readonlyPageWidgetConfigLocalVar, widgetCategory, icon);
type.setTitles(titles);
type.setConfig(defaultConfig);
diff --git a/engine/src/main/java/org/entando/entando/aps/tags/FreemarkerTemplateParameterTag.java b/engine/src/main/java/org/entando/entando/aps/tags/FreemarkerTemplateParameterTag.java
index 03c135e773..3d67abf429 100644
--- a/engine/src/main/java/org/entando/entando/aps/tags/FreemarkerTemplateParameterTag.java
+++ b/engine/src/main/java/org/entando/entando/aps/tags/FreemarkerTemplateParameterTag.java
@@ -16,7 +16,7 @@
import com.agiletec.aps.system.RequestContext;
import freemarker.core.Environment;
import freemarker.ext.beans.StringModel;
-import freemarker.ext.servlet.AllHttpScopesHashModel;
+import freemarker.ext.jakarta.servlet.AllHttpScopesHashModel;
import freemarker.template.TemplateModel;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.jsp.JspException;
diff --git a/engine/src/main/java/org/entando/entando/ent/util/LabelSanitizer.java b/engine/src/main/java/org/entando/entando/ent/util/LabelSanitizer.java
new file mode 100644
index 0000000000..974bb480f4
--- /dev/null
+++ b/engine/src/main/java/org/entando/entando/ent/util/LabelSanitizer.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2015-Present Entando Inc. (http://www.entando.com) All rights reserved.
+ *
+ * This library is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+ * details.
+ */
+package org.entando.entando.ent.util;
+
+import java.util.ArrayList;
+import com.agiletec.aps.util.ApsProperties;
+
+/**
+ * Neutralizes stored XSS in short free-text label fields (titles, names,
+ * descriptions) by removing the characters that allow HTML element injection.
+ */
+public final class LabelSanitizer {
+
+ private LabelSanitizer() {
+ }
+
+ /**
+ * The single quote is intentionally preserved, being legitimate and common in
+ * titles/descriptions; all known attribute sinks for these fields are double-quoted.
+ * Null-safe and idempotent.
+ */
+ public static String stripMarkup(String value) {
+ return (value == null) ? null
+ : value.replace("<", "")
+ .replace(">", "")
+ .replace("\"", "")
+ .replace("<", "")
+ .replace(">", "")
+ .replace(""", "");
+ }
+
+ /**
+ * Applies {@link #stripMarkup(String)} to every String value of the given properties
+ * in place (e.g. the per-language titles/names maps).
+ */
+ public static void stripMarkup(ApsProperties properties) {
+ if (null == properties) {
+ return;
+ }
+ for (Object key : new ArrayList<>(properties.keySet())) {
+ Object value = properties.get(key);
+ if (value instanceof String s) {
+ properties.put(key, stripMarkup(s));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java
index 86fd6f20e2..1208f1598b 100644
--- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java
+++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java
@@ -7,6 +7,8 @@
import static org.entando.entando.keycloak.services.mapping.DynamicMappingKind.ROLEGROUP;
import static org.entando.entando.keycloak.services.mapping.DynamicMappingKind.ROLEGROUPCLAIM;
+import java.util.regex.Pattern;
+
import com.agiletec.aps.system.common.AbstractService;
import com.agiletec.aps.system.services.authorization.Authorization;
import com.agiletec.aps.system.services.authorization.AuthorizationManager;
@@ -37,6 +39,7 @@
import org.entando.entando.keycloak.services.mapping.DynamicMapping;
import org.entando.entando.keycloak.services.mapping.DynamicMappingElement;
import org.entando.entando.keycloak.services.mapping.DynamicMappingKind;
+import org.entando.entando.keycloak.services.mapping.FallbackKind;
import org.entando.entando.keycloak.services.mapping.PersistKind;
import org.entando.entando.keycloak.services.oidc.OidcMappingHelper;
import org.entando.entando.keycloak.services.oidc.model.KeycloakUser;
@@ -102,6 +105,7 @@ public void initTenantAware() throws Exception {
List ignore = new ArrayList<>();
List roles = new ArrayList<>();
List groups = new ArrayList<>();
+ List excludeUsers = new ArrayList<>();
Boolean enabled = false;
PersistKind persist = PersistKind.FULL;
@@ -130,6 +134,8 @@ public void initTenantAware() throws Exception {
.orElseGet(List::of);
groups = ofNullable(dynConf.groups)
.orElse(List.of());
+ excludeUsers = ofNullable(dynConf.excludeUsers)
+ .orElse(List.of());
enabled = ofNullable(dynConf.enabled)
.orElse(false);
persist = ofNullable(dynConf.persist)
@@ -143,12 +149,12 @@ public void initTenantAware() throws Exception {
jwtMappings.forEach(m -> log.debug("jwt mapping active: {}", m.toString()));
}
// finally
- KeycloakImportConfig cfg = new KeycloakImportConfig(profileMappings, jwtMappings, ignore, roles, groups, enabled, persist);
+ KeycloakImportConfig cfg = new KeycloakImportConfig(profileMappings, jwtMappings, ignore, roles, groups, enabled, persist, excludeUsers);
setImportConfiguration(cfg);
} catch (Exception e) {
log.error("Error initializing KeycloakAuthorizationManager", e);
- KeycloakImportConfig cfg = new KeycloakImportConfig(List.of(), List.of(), List.of(), List.of(), List.of(), false, PersistKind.NONE);
+ KeycloakImportConfig cfg = new KeycloakImportConfig(List.of(), List.of(), List.of(), List.of(), List.of(), false, PersistKind.NONE, List.of());
setImportConfiguration(cfg);
throw e;
@@ -190,23 +196,36 @@ public void cleanSyncData() {
* @return true if the element is valid, false otherwise
*/
private boolean isValid(DynamicMappingElement elem) {
- if (elem.kind == null) {
- log.error("invalid dynamic mapping element, 'kind' is blank");
- return false;
- }
- if (StringUtils.isBlank(elem.attribute) && !elem.kind.isJwtMapping()) {
- log.error("invalid dynamic mapping element, 'attribute' is blank for kind {}", elem.kind);
- return false;
- }
- if (StringUtils.isBlank(elem.path) && elem.kind.isJwtMapping()) {
- log.error("invalid dynamic mapping element, 'path' is blank for {} kind", elem.kind);
- return false;
- }
- if (StringUtils.isBlank(elem.separator) && (elem.kind == ROLEGROUP || elem.kind == ROLEGROUPCLAIM)) {
- log.error("invalid dynamic mapping element, 'separator' is blank for {} kind", elem.kind);
+ try {
+ if (elem == null) {
+ log.error("invalid dynamic mapping element, element is null");
+ return false;
+ }
+ if (elem.kind == null) {
+ log.error("invalid dynamic mapping element, 'kind' is missing");
+ return false;
+ }
+ if (StringUtils.isBlank(elem.attribute) && !elem.kind.isJwtMapping()) {
+ log.error("invalid dynamic mapping element, 'attribute' is blank for kind {}", elem.kind);
+ return false;
+ }
+ if (StringUtils.isBlank(elem.path) && elem.kind.isJwtMapping()) {
+ log.error("invalid dynamic mapping element, 'path' is blank for {} kind", elem.kind);
+ return false;
+ }
+ if (StringUtils.isBlank(elem.separator) && (elem.kind == ROLEGROUP || elem.kind == ROLEGROUPCLAIM)) {
+ log.error("invalid dynamic mapping element, 'separator' is blank for {} kind", elem.kind);
+ return false;
+ }
+ if (elem.fallback != null && elem.kind != ROLEGROUP && elem.kind != ROLEGROUPCLAIM) {
+ log.warn("dynamic mapping element has 'fallback' set for kind {} which does not support it; "
+ + "'fallback' is only applicable to {} and {}", elem.kind, ROLEGROUP, ROLEGROUPCLAIM);
+ }
+ return true;
+ } catch (Exception e) {
+ log.error("Unexpected error validating dynamic mapping element", e);
return false;
}
- return true;
}
public void processNewUser(final UserDetails user, final String token, final boolean decode) {
@@ -216,6 +235,11 @@ public void processNewUser(final UserDetails user, final String token, final boo
readLock.lock();
try {
+ final List excludedUsers = getImportConfiguration().getExcludeUsers();
+ if (excludedUsers != null && user.getUsername() != null && excludedUsers.contains(user.getUsername())) {
+ log.debug("user {} is in the excludeUsers list, skipping dynamic sync", user.getUsername());
+ return;
+ }
if (!getImportConfiguration().getEnabled()) return;
// Authorizations coming from dynamic mapping (that is, external sources)
@@ -336,19 +360,18 @@ private List finalizeGroupRoleAssociation(KeycloakUser user, Dyna
}
final String sep = StringUtils.isNotBlank(elem.separator) ? elem.separator : DEFAULT_SEPARATOR;
- final String[] tokens = candidate.split(sep);
- if (tokens.length < 2) {
- // treat as a role
-// result.addAll(finalizeRoleAssociation(user, elem, List.of(candidate.trim())));
+ if (!candidate.contains(sep)) {
+ result.addAll(applyFallbackForRoleGroup(user, elem, candidate.trim()));
continue;
}
+ final String[] tokens = candidate.split(Pattern.quote(sep), -1);
final String roleName = tokens[0].trim();
- final String groupName = tokens[1].trim();
+ final String groupName = tokens.length > 1 ? tokens[1].trim() : "";
- if (StringUtils.isBlank(roleName)) {
- log.warn("Invalid role name extracted from candidate '{}' for user {}", candidate, user.getUsername());
+ if (StringUtils.isBlank(roleName) || StringUtils.isBlank(groupName)) {
+ log.warn("Blank role or group in token '{}' for user {}, discarding", candidate, user.getUsername());
continue;
}
@@ -363,6 +386,20 @@ private List finalizeGroupRoleAssociation(KeycloakUser user, Dyna
return result;
}
+ private List applyFallbackForRoleGroup(KeycloakUser user, DynamicMappingElement elem, String token) {
+ if (elem.fallback == FallbackKind.IGNORE) {
+ log.warn("No separator in token '{}' for user {}, discarding (fallback=ignore)", token, user.getUsername());
+ return List.of();
+ }
+ if (elem.fallback == FallbackKind.GROUP) {
+ return finalizeGroupAssociation(user, elem, List.of(token));
+ }
+ if (elem.fallback == null) {
+ log.info("No separator in token '{}' for user {}, treating as role (implicit default fallback)", token, user.getUsername());
+ }
+ return finalizeRoleAssociation(user, elem, List.of(token));
+ }
+
private void processNewUser(final UserDetails user) {
if (StringUtils.isNotEmpty(KeycloackConfiguration.getDefaultAuthorizations())) {
// process group and role coming from the configuration
@@ -471,6 +508,10 @@ private List doProcessRoleGroup(KeycloakUser user, DynamicMapping
return result;
}
for (String groupRoleToken : authorizations) {
+ if (!groupRoleToken.contains(separator)) {
+ result.addAll(applyFallbackForRoleGroup(user, elem, groupRoleToken.trim()));
+ continue;
+ }
Authorization auth = parseAuthForRoleGroup(user, groupRoleToken, separator);
if (auth != null) {
result.add(auth);
@@ -483,18 +524,16 @@ private List doProcessRoleGroup(KeycloakUser user, DynamicMapping
}
private Authorization parseAuthForRoleGroup(KeycloakUser user, String groupRoleToken, String separator) {
- final String[] tokens = groupRoleToken.split(separator);
+ final String[] tokens = groupRoleToken.split(Pattern.quote(separator), -1);
+
+ final String roleName = tokens[0].trim();
+ final String groupName = tokens.length > 1 ? tokens[1].trim() : "";
- if (tokens.length != 2
- || StringUtils.isBlank(tokens[0])
- || StringUtils.isBlank(tokens[1])) {
- log.error("invalid dynamic config configuration detected");
+ if (StringUtils.isBlank(roleName) || StringUtils.isBlank(groupName)) {
+ log.warn("Blank role or group in token '{}' for user {}, discarding", groupRoleToken, user.getUsername());
return null;
}
- final String groupName = tokens[1];
- final String roleName = tokens[0];
-
return finalizeAssociation(user, roleName, groupName, false);
}
@@ -553,11 +592,11 @@ private Authorization finalizeAssociation(KeycloakUser user, String roleName, St
}
// are they managed?
if (StringUtils.isNotBlank(roleName) && !getImportConfiguration().getRoles().contains(roleName)) {
- log.info("Role {} is not managed. Skipping assignment for user {}", roleName, user.getUsername());
+ log.warn("Role {} is not in the roles allowlist. Skipping assignment for user {}", roleName, user.getUsername());
return null;
}
if (StringUtils.isNotBlank(groupName) && !getImportConfiguration().getGroups().contains(groupName)) {
- log.info("Group {} is not managed. Skipping assignment for user {}", groupName, user.getUsername());
+ log.warn("Group {} is not in the groups allowlist. Skipping assignment for user {}", groupName, user.getUsername());
return null;
}
return createAuthorization(roleName, groupName, createRoleIfMissing);
diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakImportConfig.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakImportConfig.java
index 81053f64cb..d1d64d8396 100644
--- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakImportConfig.java
+++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakImportConfig.java
@@ -20,5 +20,6 @@ public class KeycloakImportConfig {
private transient List groups;
private transient Boolean enabled;
private transient PersistKind persist;
+ private transient List excludeUsers;
}
diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMapping.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMapping.java
index ba5b71dbc6..afdaff2656 100644
--- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMapping.java
+++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMapping.java
@@ -21,6 +21,9 @@ public class DynamicMapping {
@JacksonXmlElementWrapper(localName = "groups")
public List groups;
+ @JacksonXmlElementWrapper(localName = "excludeUsers")
+ public List excludeUsers;
+
public Boolean enabled;
public PersistKind persist;
diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java
index 229f180a23..6493c3a9d7 100644
--- a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java
+++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/DynamicMappingElement.java
@@ -7,6 +7,7 @@ public class DynamicMappingElement {
public String attribute;
public DynamicMappingKind kind;
public String separator; // FOR ROLEGROUP and ROLEGROUPCLAIM ONLY
+ public FallbackKind fallback; // FOR ROLEGROUP and ROLEGROUPCLAIM ONLY
public String path; // FOR *CLAIM ONLY
}
diff --git a/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/FallbackKind.java b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/FallbackKind.java
new file mode 100644
index 0000000000..1296845980
--- /dev/null
+++ b/keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/mapping/FallbackKind.java
@@ -0,0 +1,30 @@
+package org.entando.entando.keycloak.services.mapping;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import java.util.Arrays;
+
+public enum FallbackKind {
+ ROLE("role"),
+ GROUP("group"),
+ IGNORE("ignore");
+
+ private final String kind;
+
+ FallbackKind(String kind) {
+ this.kind = kind;
+ }
+
+ @JsonValue
+ public String getXmlValue() {
+ return kind;
+ }
+
+ @JsonCreator
+ public static FallbackKind fromValue(String value) {
+ return Arrays.stream(values())
+ .filter(k -> k.kind.equalsIgnoreCase(value))
+ .findFirst()
+ .orElse(null);
+ }
+}
diff --git a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java
index fd9dcd2cf9..0ee40a1187 100644
--- a/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java
+++ b/keycloak-plugin/src/test/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManagerTest.java
@@ -1078,6 +1078,156 @@ void testExternalAuthSyncArgsVerifiedExactly() throws Exception {
assertThat(toDeleteCaptor.getValue()).isEmpty();
}
+ // -------------------------------------------------------------------------
+ // isValid tests — enabled mappings with missing required fields
+ // -------------------------------------------------------------------------
+
+ @Test
+ void testIsValidFiltersEnabledMappingWithNullKind() throws Exception {
+ String xml = ""
+ + " true"
+ + ""
+ + " true"
+ + ""
+ + "";
+ when(configManager.getConfigItem(anyString())).thenReturn(xml);
+
+ manager.init();
+
+ assertNotNull(manager.getImportConfiguration());
+ assertThat(manager.getImportConfiguration().getEnabled()).isTrue();
+ assertThat(manager.getImportConfiguration().getProfileMappings()).isEmpty();
+ assertThat(manager.getImportConfiguration().getJwtMappings()).isEmpty();
+ }
+
+ @Test
+ void testIsValidFiltersEnabledMappingWithMissingAttribute() throws Exception {
+ String xml = ""
+ + " true"
+ + ""
+ + " truerole"
+ + ""
+ + "";
+ when(configManager.getConfigItem(anyString())).thenReturn(xml);
+
+ manager.init();
+
+ assertNotNull(manager.getImportConfiguration());
+ assertThat(manager.getImportConfiguration().getEnabled()).isTrue();
+ assertThat(manager.getImportConfiguration().getProfileMappings()).isEmpty();
+ }
+
+ @Test
+ void testIsValidFiltersEnabledMappingWithMissingPath() throws Exception {
+ String xml = ""
+ + " true"
+ + ""
+ + " trueroleclaim"
+ + ""
+ + "";
+ when(configManager.getConfigItem(anyString())).thenReturn(xml);
+
+ manager.init();
+
+ assertNotNull(manager.getImportConfiguration());
+ assertThat(manager.getImportConfiguration().getEnabled()).isTrue();
+ assertThat(manager.getImportConfiguration().getJwtMappings()).isEmpty();
+ }
+
+ @Test
+ void testIsValidFiltersEnabledMappingWithMissingSeparator() throws Exception {
+ String xml = ""
+ + " true"
+ + ""
+ + " trueAD_GROUPROLErolegroup"
+ + ""
+ + "";
+ when(configManager.getConfigItem(anyString())).thenReturn(xml);
+
+ manager.init();
+
+ assertNotNull(manager.getImportConfiguration());
+ assertThat(manager.getImportConfiguration().getEnabled()).isTrue();
+ assertThat(manager.getImportConfiguration().getProfileMappings()).isEmpty();
+ }
+
+ // -------------------------------------------------------------------------
+ // isValid — fallback on unsupported kind: warning only, mapping kept
+ // -------------------------------------------------------------------------
+
+ @Test
+ void testIsValidWarnsFallbackOnUnsupportedKind() throws Exception {
+ String xml = ""
+ + " true"
+ + ""
+ + " "
+ + " true"
+ + " realm_access.roles"
+ + " roleclaim"
+ + " role"
+ + " "
+ + ""
+ + "generico"
+ + "";
+ when(configManager.getConfigItem(anyString())).thenReturn(xml);
+
+ manager.init();
+
+ assertNotNull(manager.getImportConfiguration());
+ // The mapping must NOT be rejected — fallback warning is non-fatal
+ assertThat(manager.getImportConfiguration().getJwtMappings()).hasSize(1);
+ }
+
+ // -------------------------------------------------------------------------
+ // Case-insensitivity: uppercase kind and persist identifiers
+ // -------------------------------------------------------------------------
+
+ @Test
+ void testConfigAcceptsUppercaseKind() throws Exception {
+ String xml = ""
+ + " true"
+ + " full"
+ + ""
+ + " "
+ + " true"
+ + " realm_access.roles"
+ + " ROLECLAIM"
+ + " "
+ + ""
+ + "generico"
+ + "";
+ when(configManager.getConfigItem(anyString())).thenReturn(xml);
+
+ manager.init();
+
+ assertNotNull(manager.getImportConfiguration());
+ assertThat(manager.getImportConfiguration().getEnabled()).isTrue();
+ assertThat(manager.getImportConfiguration().getJwtMappings()).hasSize(1);
+ }
+
+ @Test
+ void testConfigAcceptsUppercasePersist() throws Exception {
+ String xml = ""
+ + " true"
+ + " FULL"
+ + ""
+ + " "
+ + " true"
+ + " realm_access.roles"
+ + " roleclaim"
+ + " "
+ + ""
+ + "generico"
+ + "";
+ when(configManager.getConfigItem(anyString())).thenReturn(xml);
+
+ manager.init();
+
+ assertNotNull(manager.getImportConfiguration());
+ assertThat(manager.getImportConfiguration().getPersist())
+ .isEqualTo(org.entando.entando.keycloak.services.mapping.PersistKind.FULL);
+ }
+
private Authorization authorization(final String groupName, final String roleName) {
final Group group = new Group();
group.setName(groupName);
@@ -1459,6 +1609,7 @@ void testCleanSyncDataDisabled() throws Exception {
+ " realm_access.roles"
+ " ROLEGROUPCLAIM"
+ " _SEP_"
+ + " ignore"
+ " "
+ ""
+ ""
@@ -1600,6 +1751,222 @@ void testCleanSyncDataDisabled() throws Exception {
+ " "
+ "";
+ // -------------------------------------------------------------------------
+ // excludeUsers tests
+ // -------------------------------------------------------------------------
+
+ @Test
+ void testExcludeUsersSkipsDynamicSync() throws Exception {
+ String xml = ""
+ + " FULL"
+ + " true"
+ + ""
+ + " "
+ + " true"
+ + " realm_access.roles"
+ + " ROLECLAIM"
+ + " "
+ + ""
+ + ""
+ + " service-account"
+ + ""
+ + "generico"
+ + "";
+
+ when(configuration.getDefaultAuthorizations()).thenReturn(null);
+ when(configManager.getConfigItem(anyString())).thenReturn(xml);
+ when(userDetails.getUsername()).thenReturn("service-account");
+ lenient().when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>());
+
+ manager.init();
+ manager.processNewUser(userDetails, JWT, false);
+
+ verify(authorizationManager, never()).externalAuthSync(anyString(), anyLong(), anyList(), anyList());
+ verify(userDetails, never()).addAuthorizations(anyList());
+ }
+
+ // -------------------------------------------------------------------------
+ // fallback tests — rolegroupclaim (JWT path)
+ // -------------------------------------------------------------------------
+
+ private static final String JWT_SINGLE_TOKEN =
+ "{ \"iat\":1768319143, \"realm_access\":{ \"roles\":[\"singletoken\"] } }";
+
+ @Test
+ void testFallbackDefaultTreatsTokenAsRole() throws Exception {
+ // No → default → treat as role + INFO log
+ String xml = ""
+ + " NONE"
+ + " true"
+ + ""
+ + " "
+ + " true"
+ + " realm_access.roles"
+ + " ROLEGROUPCLAIM"
+ + " _SEP_"
+ + " "
+ + ""
+ + "singletoken"
+ + ""
+ + "";
+
+ when(configuration.getDefaultAuthorizations()).thenReturn(null);
+ when(configManager.getConfigItem(anyString())).thenReturn(xml);
+ when(userDetails.getUsername()).thenReturn("testuser");
+ when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>());
+
+ manager.init();
+ manager.processNewUser(userDetails, JWT_SINGLE_TOKEN, false);
+
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor> listCaptor = ArgumentCaptor.forClass(List.class);
+ verify(userDetails, times(1)).addAuthorizations(listCaptor.capture());
+ assertThat(listCaptor.getValue()).hasSize(1);
+ assertThat(listCaptor.getValue().get(0).getRole().getName()).isEqualTo("singletoken");
+ assertThat(listCaptor.getValue().get(0).getGroup()).isNull();
+ }
+
+ @Test
+ void testFallbackRoleTreatsTokenAsRole() throws Exception {
+ String xml = ""
+ + " NONE"
+ + " true"
+ + ""
+ + " "
+ + " true"
+ + " realm_access.roles"
+ + " ROLEGROUPCLAIM"
+ + " _SEP_"
+ + " role"
+ + " "
+ + ""
+ + "singletoken"
+ + ""
+ + "";
+
+ when(configuration.getDefaultAuthorizations()).thenReturn(null);
+ when(configManager.getConfigItem(anyString())).thenReturn(xml);
+ when(userDetails.getUsername()).thenReturn("testuser");
+ when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>());
+
+ manager.init();
+ manager.processNewUser(userDetails, JWT_SINGLE_TOKEN, false);
+
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor> listCaptor = ArgumentCaptor.forClass(List.class);
+ verify(userDetails, times(1)).addAuthorizations(listCaptor.capture());
+ assertThat(listCaptor.getValue()).hasSize(1);
+ assertThat(listCaptor.getValue().get(0).getRole().getName()).isEqualTo("singletoken");
+ assertThat(listCaptor.getValue().get(0).getGroup()).isNull();
+ }
+
+ @Test
+ void testFallbackGroupTreatsTokenAsGroup() throws Exception {
+ String xml = ""
+ + " NONE"
+ + " true"
+ + ""
+ + " "
+ + " true"
+ + " realm_access.roles"
+ + " ROLEGROUPCLAIM"
+ + " _SEP_"
+ + " group"
+ + " "
+ + ""
+ + ""
+ + "singletoken"
+ + "";
+
+ when(configuration.getDefaultAuthorizations()).thenReturn(null);
+ when(configManager.getConfigItem(anyString())).thenReturn(xml);
+ when(userDetails.getUsername()).thenReturn("testuser");
+ when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>());
+
+ manager.init();
+ manager.processNewUser(userDetails, JWT_SINGLE_TOKEN, false);
+
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor> listCaptor = ArgumentCaptor.forClass(List.class);
+ verify(userDetails, times(1)).addAuthorizations(listCaptor.capture());
+ assertThat(listCaptor.getValue()).hasSize(1);
+ assertThat(listCaptor.getValue().get(0).getGroup().getName()).isEqualTo("singletoken");
+ assertThat(listCaptor.getValue().get(0).getRole()).isNull();
+ }
+
+ @Test
+ void testFallbackIgnoreDiscardsToken() throws Exception {
+ String xml = ""
+ + " NONE"
+ + " true"
+ + ""
+ + " "
+ + " true"
+ + " realm_access.roles"
+ + " ROLEGROUPCLAIM"
+ + " _SEP_"
+ + " ignore"
+ + " "
+ + ""
+ + "singletoken"
+ + ""
+ + "";
+
+ when(configuration.getDefaultAuthorizations()).thenReturn(null);
+ when(configManager.getConfigItem(anyString())).thenReturn(xml);
+ when(userDetails.getUsername()).thenReturn("testuser");
+ when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>());
+
+ manager.init();
+ manager.processNewUser(userDetails, JWT_SINGLE_TOKEN, false);
+
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor> listCaptor = ArgumentCaptor.forClass(List.class);
+ verify(userDetails, times(1)).addAuthorizations(listCaptor.capture());
+ assertThat(listCaptor.getValue()).isEmpty();
+ }
+
+ // -------------------------------------------------------------------------
+ // fallback test — rolegroup (profile path)
+ // -------------------------------------------------------------------------
+
+ @Test
+ void testFallbackDefaultProfileRoleGroup() throws Exception {
+ String xml = ""
+ + " NONE"
+ + " true"
+ + ""
+ + " "
+ + " true"
+ + " AD_GROUPROLE"
+ + " ROLEGROUP"
+ + " _r_"
+ + " "
+ + ""
+ + "singletoken"
+ + ""
+ + "";
+
+ when(configuration.getDefaultAuthorizations()).thenReturn(null);
+ when(configManager.getConfigItem(anyString())).thenReturn(xml);
+
+ UserRepresentation userRepresentation = new UserRepresentation();
+ userRepresentation.setAttributes(Map.of("AD_GROUPROLE", List.of("singletoken")));
+ when(userDetails.getUserRepresentation()).thenReturn(userRepresentation);
+ when(userDetails.getAuthorizations()).thenReturn(new ArrayList<>());
+
+ manager.init();
+ manager.processNewUser(userDetails, null, false);
+
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor> listCaptor = ArgumentCaptor.forClass(List.class);
+ verify(userDetails, times(1)).addAuthorizations(listCaptor.capture());
+ assertThat(listCaptor.getValue()).hasSize(1);
+ assertThat(listCaptor.getValue().get(0).getRole()).isNotNull();
+ assertThat(listCaptor.getValue().get(0).getRole().getName()).isEqualTo("singletoken");
+ assertThat(listCaptor.getValue().get(0).getGroup()).isNull();
+ }
+
private static final String JWT_NO_ROLE = "{"
+ " \"header\" : {"
+ " \"alg\" : \"RS256\","
diff --git a/portal-ui/src/main/java/com/agiletec/aps/system/services/controller/control/Authenticator.java b/portal-ui/src/main/java/com/agiletec/aps/system/services/controller/control/Authenticator.java
index ac90da2695..65c97e83a8 100644
--- a/portal-ui/src/main/java/com/agiletec/aps/system/services/controller/control/Authenticator.java
+++ b/portal-ui/src/main/java/com/agiletec/aps/system/services/controller/control/Authenticator.java
@@ -101,12 +101,37 @@ public int service(RequestContext reqCtx, int status) {
}
retStatus = ControllerManager.CONTINUE;
} catch (Throwable t) {
- _logger.error("Error, could not fulfill the request", t);
+ if (isMalformedRequest(t)) {
+ // The request itself is malformed (e.g. invalid query string encoding): this is a
+ // client error, not a server fault. Log it concisely, without the full stack trace.
+ _logger.warn("Could not fulfill the request: malformed request ({})", t.getMessage());
+ } else {
+ _logger.error("Error, could not fulfill the request", t);
+ }
retStatus = ControllerManager.SYS_ERROR;
reqCtx.setHTTPError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
return retStatus;
}
+
+ /**
+ * Detects whether the given throwable (or any cause in its chain) denotes a malformed client
+ * request, such as an invalid query string encoding surfaced while accessing request parameters.
+ * Such conditions are client errors (HTTP 400) and don't warrant a full ERROR stack trace.
+ * Matching is done by class name to avoid a hard dependency on the servlet container internals.
+ */
+ private static boolean isMalformedRequest(Throwable t) {
+ for (Throwable cause = t; cause != null; cause = cause.getCause()) {
+ String className = cause.getClass().getSimpleName();
+ if ("BadMessageException".equals(className) || cause instanceof IllegalArgumentException) {
+ return true;
+ }
+ if (cause.getCause() == cause) {
+ break;
+ }
+ }
+ return false;
+ }
protected IUserManager getUserManager() {
return _userManager;
diff --git a/redis-plugin/docker-compose-sentinel.yaml b/redis-plugin/docker-compose-sentinel.yaml
index d5309e8dee..64b4ebbf12 100644
--- a/redis-plugin/docker-compose-sentinel.yaml
+++ b/redis-plugin/docker-compose-sentinel.yaml
@@ -6,7 +6,7 @@ networks:
services:
redis:
- image: 'bitnami/redis:latest'
+ image: 'bitnamilegacy/redis:7.0.8-debian-11-r12'
environment:
- REDIS_REPLICATION_MODE=master
- REDIS_PASSWORD=str0ng_passw0rd
@@ -15,7 +15,7 @@ services:
ports:
- '6379'
redis-slave:
- image: 'bitnami/redis:latest'
+ image: 'bitnamilegacy/redis:7.0.8-debian-11-r12'
environment:
- REDIS_REPLICATION_MODE=slave
- REDIS_MASTER_HOST=redis
@@ -31,6 +31,7 @@ services:
image: 'bitnami/redis-sentinel:latest'
environment:
- REDIS_MASTER_PASSWORD=str0ng_passw0rd
+ - ALLOW_EMPTY_PASSWORD=yes
depends_on:
- redis
- redis-slave
diff --git a/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/content/AdvContentFacetManager.java b/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/content/AdvContentFacetManager.java
index d903ea29c8..1644d79623 100644
--- a/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/content/AdvContentFacetManager.java
+++ b/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/content/AdvContentFacetManager.java
@@ -22,6 +22,7 @@
import com.agiletec.plugins.jacms.aps.system.services.searchengine.ICmsSearchEngineManager;
import java.util.ArrayList;
import java.util.List;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
@@ -29,9 +30,15 @@
import org.entando.entando.aps.system.services.searchengine.FacetedContentsResult;
import org.entando.entando.aps.system.services.searchengine.SearchEngineFilter;
import org.entando.entando.ent.exception.EntException;
+import org.entando.entando.ent.util.EntLogging.EntLogFactory;
+import org.entando.entando.ent.util.EntLogging.EntLogger;
+import org.entando.entando.web.common.exceptions.ValidationConflictException;
+import org.entando.entando.web.common.model.Filter;
import org.entando.entando.plugins.jpsolr.aps.system.solr.ISolrSearchEngineManager;
import org.entando.entando.plugins.jpsolr.aps.system.solr.model.SolrFacetedContentsResult;
import org.entando.entando.plugins.jpsolr.aps.system.solr.model.SolrSearchEngineFilter;
+import org.entando.entando.plugins.jpsolr.web.content.model.SolrFilter;
+import org.springframework.validation.BeanPropertyBindingResult;
import org.entando.entando.plugins.jpsolr.conditions.SolrActive;
import org.entando.entando.plugins.jpsolr.web.content.model.AdvRestContentListRequest;
import org.springframework.beans.factory.annotation.Autowired;
@@ -44,6 +51,11 @@
@SolrActive(true)
public class AdvContentFacetManager implements IAdvContentFacetManager {
+ private static final EntLogger logger = EntLogFactory.getSanitizedLogger(AdvContentFacetManager.class);
+
+ private static final Pattern SAFE_IDENTIFIER = Pattern.compile("[a-zA-Z0-9_.:,-]+");
+ private static final Pattern INJECTION_PATTERN = Pattern.compile("\\$\\{|%24%7B", Pattern.CASE_INSENSITIVE);
+
private final ICategoryManager categoryManager;
private final ICmsSearchEngineManager searchEngineManager;
private final IAuthorizationManager authorizationManager;
@@ -101,11 +113,12 @@ protected SearchEngineFilter[] getFilters(SearchEngineFilter[] baseFilters, List
@Override
public SolrFacetedContentsResult getFacetedContents(AdvRestContentListRequest requestList, UserDetails user) {
+ this.validateRequest(requestList);
SolrFacetedContentsResult facetedResult;
try {
- String langCode =
- (StringUtils.isBlank(requestList.getLang())) ? this.langManager.getDefaultLang().getCode()
- : requestList.getLang();
+ String langCode = StringUtils.isBlank(requestList.getLang())
+ ? this.langManager.getDefaultLang().getCode()
+ : requestList.getLang();
SolrSearchEngineFilter[] searchFilters = requestList.extractFilters(langCode);
SolrSearchEngineFilter[][] doubleFilters = requestList.extractDoubleFilters(langCode);
if (null != searchFilters) {
@@ -124,6 +137,93 @@ public SolrFacetedContentsResult getFacetedContents(AdvRestContentListRequest re
return facetedResult;
}
+ protected void validateRequest(AdvRestContentListRequest requestList) {
+ BeanPropertyBindingResult bindingResult =
+ new BeanPropertyBindingResult(requestList, "advContentSearchRequest");
+ // lang
+ if (!StringUtils.isBlank(requestList.getLang())) {
+ boolean validLang = this.langManager.getLangs().stream()
+ .anyMatch(l -> l.getCode().equals(requestList.getLang()));
+ if (!validLang) {
+ bindingResult.rejectValue("lang", "INVALID_LANG_CODE",
+ new Object[]{}, "lang.code.invalid");
+ }
+ }
+ // sort
+ rejectIfUnsafeIdentifier(requestList.getSort(), "sort", bindingResult);
+ // direction
+ rejectIfUnsafeIdentifier(requestList.getDirection(), "direction", bindingResult);
+ // text
+ rejectIfInjection(requestList.getText(), "text", bindingResult);
+ // searchOption
+ rejectIfUnsafeIdentifier(requestList.getSearchOption(), "searchOption", bindingResult);
+ // csvCategories
+ if (null != requestList.getCsvCategories()) {
+ for (String csv : requestList.getCsvCategories()) {
+ rejectIfUnsafeIdentifier(csv, "csvCategories", bindingResult);
+ }
+ }
+ // filters
+ validateFilters(requestList.getFilters(), "filters", bindingResult);
+ // doubleFilters
+ if (null != requestList.getDoubleFilters()) {
+ for (SolrFilter[] innerFilters : requestList.getDoubleFilters()) {
+ validateFilters(innerFilters, "doubleFilters", bindingResult);
+ }
+ }
+ if (bindingResult.hasErrors()) {
+ throw new ValidationConflictException(bindingResult);
+ }
+ }
+
+ private void validateFilters(Filter[] filters, String fieldPrefix,
+ BeanPropertyBindingResult bindingResult) {
+ if (null == filters) {
+ return;
+ }
+ for (Filter filter : filters) {
+ rejectIfUnsafeIdentifier(filter.getAttribute(), fieldPrefix + ".attribute", bindingResult);
+ rejectIfUnsafeIdentifier(filter.getEntityAttr(), fieldPrefix + ".entityAttr", bindingResult);
+ rejectIfUnsafeIdentifier(filter.getOperator(), fieldPrefix + ".operator", bindingResult);
+ rejectIfUnsafeIdentifier(filter.getOrder(), fieldPrefix + ".order", bindingResult);
+ rejectIfUnsafeIdentifier(filter.getType(), fieldPrefix + ".type", bindingResult);
+ rejectIfInjection(filter.getValue(), fieldPrefix + ".value", bindingResult);
+ if (null != filter.getAllowedValues()) {
+ for (String av : filter.getAllowedValues()) {
+ rejectIfInjection(av, fieldPrefix + ".allowedValues", bindingResult);
+ }
+ }
+ if (filter instanceof SolrFilter solrFilter) {
+ rejectIfUnsafeIdentifier(solrFilter.getSearchOption(),
+ fieldPrefix + ".searchOption", bindingResult);
+ }
+ }
+ }
+
+ private static void rejectIfUnsafeIdentifier(String value, String field,
+ BeanPropertyBindingResult bindingResult) {
+ if (StringUtils.isBlank(value)) {
+ return;
+ }
+ if (!SAFE_IDENTIFIER.matcher(value).matches()) {
+ logger.warn("Rejected unsafe identifier in field '{}': '{}'", field, value);
+ bindingResult.rejectValue(null, "INVALID_PARAMETER",
+ new Object[]{field}, "parameter.invalid");
+ }
+ }
+
+ private static void rejectIfInjection(String value, String field,
+ BeanPropertyBindingResult bindingResult) {
+ if (StringUtils.isBlank(value)) {
+ return;
+ }
+ if (INJECTION_PATTERN.matcher(value).find()) {
+ logger.warn("Rejected injection pattern in field '{}': '{}'", field, value);
+ bindingResult.rejectValue(null, "INVALID_PARAMETER",
+ new Object[]{field}, "parameter.invalid");
+ }
+ }
+
protected List getAllowedGroups(UserDetails currentUser) {
List groupCodes = new ArrayList<>();
if (null != currentUser) {
diff --git a/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/solr/SearcherDAO.java b/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/solr/SearcherDAO.java
index ad46976d49..db41072738 100644
--- a/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/solr/SearcherDAO.java
+++ b/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/solr/SearcherDAO.java
@@ -30,6 +30,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
@@ -65,6 +66,12 @@
@Slf4j
public class SearcherDAO implements ISolrSearcherDAO {
+ // Allowlist for Lucene/Solr field names: only alphanumeric and underscore.
+ // Parentheses, spaces, operators (AND/OR/NOT), and other Lucene meta-characters
+ // in a field name break the serialized query string produced by BooleanQuery.toString()
+ // and are the root of the Lucene query injection vulnerability.
+ private static final Pattern VALID_FIELD_KEY = Pattern.compile("[a-zA-Z0-9_]+");
+
private ITreeNodeManager treeNodeManager;
private ILangManager langManager;
@@ -175,16 +182,26 @@ protected SolrFacetedContentsResult executeQuery(Query query, SearchEngineFilter
}
private void addFilters(SolrQuery solrQuery, SearchEngineFilter[] filters) {
- if (null != filters) {
- for (SearchEngineFilter> filter : filters) {
- if (null != this.getRelevance(filter)) {
- solrQuery.addSort("score", ORDER.desc);
- } else if (null != filter.getOrder()) {
- String fieldKey = this.getFilterKey(filter);
- boolean revert = filter.getOrder().toString().equalsIgnoreCase("DESC");
- solrQuery.addSort(fieldKey, (revert) ? ORDER.desc : ORDER.asc);
- }
- }
+ if (null == filters) {
+ return;
+ }
+ for (SearchEngineFilter> filter : filters) {
+ this.addSort(solrQuery, filter);
+ }
+ }
+
+ private void addSort(SolrQuery solrQuery, SearchEngineFilter> filter) {
+ if (null != this.getRelevance(filter)) {
+ solrQuery.addSort("score", ORDER.desc);
+ return;
+ }
+ if (null == filter.getOrder()) {
+ return;
+ }
+ String fieldKey = this.getFilterKey(filter);
+ if (null != fieldKey) {
+ boolean revert = filter.getOrder().toString().equalsIgnoreCase("DESC");
+ solrQuery.addSort(fieldKey, revert ? ORDER.desc : ORDER.asc);
}
}
@@ -327,6 +344,9 @@ protected Query createQueryByFilter(SearchEngineFilter> filter) {
return null;
}
String key = this.getFilterKey(filter);
+ if (null == key) {
+ return null;
+ }
Object value = filter.getValue();
List> allowedValues = filter.getAllowedValues();
Integer relevanceValue = this.getRelevance(filter);
@@ -541,6 +561,10 @@ protected Query getTermQueryForTextSearch(String key, String value, boolean isLi
protected String getFilterKey(SearchEngineFilter> filter) {
String key = filter.getKey().replace(":", "_");
+ if (!VALID_FIELD_KEY.matcher(key).matches()) {
+ log.warn("Rejected Solr field key with unsafe characters: '{}'", key);
+ return null;
+ }
if (filter.isFullTextSearch()) {
return key;
}
@@ -548,7 +572,12 @@ protected String getFilterKey(SearchEngineFilter> filter) {
String insertedLangCode = filter.getLangCode();
String langCode = (StringUtils.isBlank(insertedLangCode)) ? this.getLangManager().getDefaultLang().getCode()
: insertedLangCode;
- key = langCode.toLowerCase() + "_" + key;
+ String normalizedLang = langCode.toLowerCase();
+ if (!VALID_FIELD_KEY.matcher(normalizedLang).matches()) {
+ log.warn("Rejected Solr lang code with unsafe characters: '{}'", normalizedLang);
+ return null;
+ }
+ key = normalizedLang + "_" + key;
} else if (!key.startsWith(SolrFields.SOLR_FIELD_PREFIX)) {
key = SolrFields.SOLR_FIELD_PREFIX + key;
}
diff --git a/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/solr/SolrFieldsChecker.java b/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/solr/SolrFieldsChecker.java
index 976c744488..74a81f268b 100644
--- a/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/solr/SolrFieldsChecker.java
+++ b/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/solr/SolrFieldsChecker.java
@@ -83,6 +83,13 @@ private void refreshBaseFields() {
private void checkLangFields() {
for (Lang lang : this.languages) {
this.checkField(lang.getCode(), SolrFields.TYPE_TEXT_GENERAL, true);
+ // The per-language attachment field ("_attachment") is written by IndexerDAO
+ // and queried by SearcherDAO when includeAttachments=true. It must exist in the
+ // schema up front; otherwise a full-text search with includeAttachments on an
+ // environment where no attachment content has been indexed queries an unknown field
+ // and fails. Same type/multiplicity as the main language field.
+ this.checkField(lang.getCode() + SolrFields.ATTACHMENT_FIELD_SUFFIX,
+ SolrFields.TYPE_TEXT_GENERAL, true);
}
}
diff --git a/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/solr/model/SolrFacetedContentsResult.java b/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/solr/model/SolrFacetedContentsResult.java
index d5ac79cfb0..26cdaeb853 100644
--- a/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/solr/model/SolrFacetedContentsResult.java
+++ b/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/aps/system/solr/model/SolrFacetedContentsResult.java
@@ -20,7 +20,10 @@
*/
public class SolrFacetedContentsResult extends FacetedContentsResult {
- private Integer totalSize;
+ // Defaults to 0 so a search that returns no result - including one where the Solr query
+ // failed and the exception was swallowed in SearcherDAO.executeQuery - still yields a valid
+ // total. A null here would make PagedMetadata throw an NPE while building the response.
+ private Integer totalSize = 0;
public SolrFacetedContentsResult() {
super();
diff --git a/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/web/content/AdvContentSearchController.java b/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/web/content/AdvContentSearchController.java
index 75f05271fe..154e594732 100644
--- a/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/web/content/AdvContentSearchController.java
+++ b/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/web/content/AdvContentSearchController.java
@@ -27,6 +27,7 @@
import org.entando.entando.plugins.jpsolr.web.content.model.AdvRestContentListRequest;
import org.entando.entando.plugins.jpsolr.web.content.model.SolrContentPagedMetadata;
import org.entando.entando.plugins.jpsolr.web.content.model.SolrFacetedPagedMetadata;
+import org.entando.entando.web.common.exceptions.ValidationGenericException;
import org.entando.entando.web.common.model.PagedRestResponse;
import org.entando.entando.web.common.model.RestResponse;
import org.entando.entando.web.common.validator.AbstractPaginationValidator;
@@ -34,6 +35,7 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
+import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
@@ -76,6 +78,11 @@ protected String getDefaultSortProperty() {
@Override
public boolean isValidField(String fieldName, Class> type) {
+ if (fieldName == null) {
+ // full-text filters carry the search term in "value" and leave
+ // the attribute null; there is no field to validate in that case.
+ return true;
+ }
if (fieldName.contains(".")) {
return true;
} else {
@@ -90,6 +97,7 @@ public ResponseEntity> getContents(AdvRestContentListR
@RequestAttribute(value = "user", required = false) UserDetails currentUser) {
logger.debug("getting contents with request {}", requestList);
this.getPaginationValidator().validateRestListRequest(requestList, String.class);
+ this.validateSearchableFilters(requestList);
SolrFacetedContentsResult facetedResult = this.advContentFacetManager
.getFacetedContents(requestList, currentUser);
List result = facetedResult.getContentsId();
@@ -107,6 +115,7 @@ public ResponseEntity(new RestResponse<>(result, pagedMetadata), HttpStatus.OK);
}
+ /**
+ * Rejects filters that carry search intent (a value, operator, ordering or allowed values)
+ * but provide no attribute to search on. Such filters cannot be honoured; discarding them
+ * silently would return a broader result set than requested as if it were a success, so they
+ * are reported as a bad request. Empty placeholder filters (no intent at all) are tolerated
+ * and dropped later during filter extraction.
+ */
+ private void validateSearchableFilters(AdvRestContentListRequest requestList) {
+ if (requestList.hasMalformedFilters()) {
+ BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(requestList, "requestList");
+ bindingResult.reject(AbstractPaginationValidator.ERRCODE_FILTERING_ATTR_INVALID,
+ new Object[]{}, "filtering.filter.attr.name.invalid");
+ throw new ValidationGenericException(bindingResult);
+ }
+ }
+
}
\ No newline at end of file
diff --git a/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/web/content/model/AdvRestContentListRequest.java b/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/web/content/model/AdvRestContentListRequest.java
index 12d76420eb..0070e054eb 100644
--- a/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/web/content/model/AdvRestContentListRequest.java
+++ b/solr-plugin/src/main/java/org/entando/entando/plugins/jpsolr/web/content/model/AdvRestContentListRequest.java
@@ -21,6 +21,8 @@
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.entando.entando.aps.system.services.searchengine.SearchEngineFilter;
+import org.entando.entando.ent.util.EntLogging.EntLogFactory;
+import org.entando.entando.ent.util.EntLogging.EntLogger;
import org.entando.entando.plugins.jpsolr.aps.system.solr.model.SolrSearchEngineFilter;
import org.entando.entando.web.common.model.Filter;
import org.entando.entando.web.common.model.FilterOperator;
@@ -34,6 +36,8 @@
@ToString
public class AdvRestContentListRequest extends RestEntityListRequest {
+ private static final EntLogger logger = EntLogFactory.getSanitizedLogger(AdvRestContentListRequest.class);
+
private String lang;
private String[] csvCategories;
@@ -113,6 +117,10 @@ public SolrSearchEngineFilter[][] extractDoubleFilters(String langCode) {
for (SolrFilter[] internalFilters : df) {
SolrSearchEngineFilter[] internalSearchFilters = new SolrSearchEngineFilter[]{};
for (SolrFilter internalFilter : internalFilters) {
+ if (this.isEmptyFilter(internalFilter)) {
+ logger.warn("Discarding empty double filter with no attribute to search on");
+ continue;
+ }
SolrSearchEngineFilter> searchFilter = this.buildSearchFilter(internalFilter, langCode);
internalSearchFilters = ArrayUtils.add(internalSearchFilters, searchFilter);
}
@@ -127,6 +135,10 @@ public SolrSearchEngineFilter[] extractFilters(String langCode) {
SolrFilter[] filters = this.getFilters();
if (null != filters) {
for (SolrFilter filter : filters) {
+ if (this.isEmptyFilter(filter)) {
+ logger.warn("Discarding empty filter with no attribute to search on");
+ continue;
+ }
SolrSearchEngineFilter> searchFilter = this.buildSearchFilter(filter, langCode);
searchFilters = ArrayUtils.add(searchFilters, searchFilter);
}
@@ -154,6 +166,71 @@ private Integer getOffset() {
return this.getPageSize() * page;
}
+ /**
+ * A non-full-text filter has no field to build a Solr query key from when it carries
+ * neither an entity attribute nor a metadata attribute. Building a key from such a filter
+ * would fail with "Error: Key required". Full-text filters are never in this state: they
+ * search on the value, not on a key.
+ */
+ private boolean hasNoSearchField(SolrFilter filter) {
+ return !filter.isFullText()
+ && StringUtils.isBlank(filter.getEntityAttr())
+ && StringUtils.isBlank(filter.getAttribute());
+ }
+
+ /**
+ * Whether the filter expresses any search intent, i.e. a value or an allowed-values set.
+ * Used to tell a meaningless placeholder apart from a filter the client actually meant to
+ * apply. Operator and order are deliberately ignored: {@code operator} always defaults to a
+ * non-blank value ("like") so it is not a reliable signal, and an order without a field
+ * expresses no restriction on the result set.
+ */
+ private boolean carriesIntent(SolrFilter filter) {
+ return StringUtils.isNotBlank(filter.getValue())
+ || (filter.getAllowedValues() != null && filter.getAllowedValues().length > 0);
+ }
+
+ /**
+ * A pure no-op placeholder: no field to search on and no intent at all. These come from
+ * sparse array binding (e.g. {@code filters[99]} leaves indexes 0-98 as empty objects) and
+ * are silently discarded, because they never expressed a restriction - dropping them cannot
+ * broaden the result set.
+ */
+ private boolean isEmptyFilter(SolrFilter filter) {
+ return filter == null || (this.hasNoSearchField(filter) && !this.carriesIntent(filter));
+ }
+
+ /**
+ * A filter the client meant to apply (it carries intent) but which has no field to search
+ * on. It cannot be honoured and must not be silently dropped - doing so would return a
+ * broader result set than requested as if it were a success - so it is reported as a bad
+ * request instead.
+ */
+ private boolean isMalformedFilter(SolrFilter filter) {
+ return filter != null && this.hasNoSearchField(filter) && this.carriesIntent(filter);
+ }
+
+ /**
+ * @return true if any filter (single or double) carries search intent but has no field to
+ * search on, which the caller should reject as a bad request.
+ */
+ public boolean hasMalformedFilters() {
+ SolrFilter[] filters = this.getFilters();
+ if (null != filters && Arrays.stream(filters).anyMatch(this::isMalformedFilter)) {
+ return true;
+ }
+ SolrFilter[][] df = this.getDoubleFilters();
+ if (null != df) {
+ for (SolrFilter[] internalFilters : df) {
+ if (null != internalFilters
+ && Arrays.stream(internalFilters).anyMatch(this::isMalformedFilter)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
private SolrSearchEngineFilter> buildSearchFilter(SolrFilter filter, String langCode) {
SolrSearchEngineFilter