CVE-2025-52472
Description
XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. Starting in version 4.3-milestone-1 and prior to versions 16.10.9, 17.4.2, and 17.5.0, the REST search URL is vulnerable to HQL injection via the orderField parameter. The specified value is added twice in the query, though, once in the field list for the select and once in the order clause, so it's not that easy to exploit. The part of the query between the two fields can be enclosed in single quotes to effectively remove them, but the query still needs to remain valid with the query two times in it. This has been patched in versions 17.5.0, 17.4.2, and 16.10.9. No known workarounds are available.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.xwiki.platform:xwiki-platform-rest-serverMaven | >= 17.0.0-rc-1, < 17.4.2 | 17.4.2 |
org.xwiki.platform:xwiki-platform-rest-serverMaven | >= 4.3-milestone-1, < 16.10.9 | 16.10.9 |
Affected products
1Patches
489e08a6422ec55ef6325bb07a45eca2af772XWIKI-23247: Improve REST search query execution
1 file changed · +94 −78
xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/main/java/org/xwiki/rest/internal/resources/DatabaseKeywordSearchSource.java+94 −78 modified@@ -103,6 +103,9 @@ public class DatabaseKeywordSearchSource implements KeywordSearchSource @Inject private EntityReferenceProvider defaultEntityReferenceProvider; + @Inject + private QueryManager queryManager; + @Inject @Named("secure") private QueryManager secureQueryManager; @@ -162,6 +165,8 @@ private List<SearchResult> searchPages(KeywordSearchOptions options, String keyw return new ArrayList<>(); } + QueryManager finalQueryManager = this.queryManager; + String orderField = options.orderField(); /* * If the order field is already one of the field hard coded in the base query, then do not add it to the @@ -246,13 +251,17 @@ private List<SearchResult> searchPages(KeywordSearchOptions options, String keyw } else { orderClause = String.format("doc.%s %s", orderField, HqlQueryUtils.getValidQueryOrder(options.order(), "asc")); + + if (!StringUtils.isAlphanumeric(orderField)) { + finalQueryManager = this.secureQueryManager; + } } // Add ordering f.format(") order by %s", orderClause); String queryString = f.toString(); - Query query = this.secureQueryManager.createQuery(queryString, Query.HQL) + Query query = finalQueryManager.createQuery(queryString, Query.HQL) .bindValue("keywords", String.format("%%%s%%", keywords.toUpperCase())) .addFilter(this.hiddenDocumentFilterProvider.get()).setOffset(options.start()) // Worst case scenario when making the locale aware query: @@ -394,7 +403,7 @@ private List<SearchResult> searchSpaces(String keywords, String wikiName, int nu + " or lower(space.reference) like lower(:prefix) escape '!'" + " order by lower(space.reference), space.reference"; - List<Object> queryResult = this.secureQueryManager.createQuery(query, Query.HQL) + List<Object> queryResult = this.queryManager.createQuery(query, Query.HQL) .bindValue("keywords", String.format("%%%s%%", escapedKeywords)) .bindValue("prefix", String.format("%s%%", escapedKeywords)) .setWiki(wikiName).setLimit(number).setOffset(start) @@ -454,17 +463,15 @@ private List<SearchResult> searchObjects(String keywords, KeywordSearchOptions o { XWikiContext context = this.contextProvider.get(); - XWiki xwikiApi = new XWiki(context.getWiki(), context); - String database = context.getWikiId(); try (Formatter f = new Formatter()) { - List<SearchResult> result = new ArrayList<SearchResult>(); - if (keywords == null) { - return result; + return new ArrayList<SearchResult>(); } + QueryManager finalQueryManager = this.queryManager; + String orderField = options.orderField(); /* * If the order field is already one of the field hard coded in the base query, then do not add it to the @@ -491,15 +498,15 @@ private List<SearchResult> searchObjects(String keywords, KeywordSearchOptions o } /* Build the order clause. */ - String orderClause = null; + String orderClause; if (StringUtils.isBlank(orderField)) { orderClause = "doc.fullName asc"; } else { - /* Check if the order parameter is a valid "asc" or "desc" string, otherwise use "asc" */ - if ("asc".equals(options.order()) || "desc".equals(options.order())) { - orderClause = String.format("doc.%s %s", orderField, options.order()); - } else { - orderClause = String.format("doc.%s asc", orderField); + orderClause = + String.format("doc.%s %s", orderField, HqlQueryUtils.getValidQueryOrder(options.order(), "asc")); + + if (!StringUtils.isAlphanumeric(orderField)) { + finalQueryManager = this.secureQueryManager; } } @@ -520,85 +527,94 @@ private List<SearchResult> searchObjects(String keywords, KeywordSearchOptions o exception */ if (options.space() != null) { queryResult = - this.secureQueryManager.createQuery(query, Query.XWQL) + finalQueryManager.createQuery(query, Query.XWQL) .bindValue("keywords", String.format("%%%s%%", keywords.toUpperCase())) .bindValue("space", options.space()).setLimit(options.number()).execute(); } else { queryResult = - this.secureQueryManager.createQuery(query, Query.XWQL) + finalQueryManager.createQuery(query, Query.XWQL) .bindValue("keywords", String.format("%%%s%%", keywords.toUpperCase())) .setLimit(options.number()) .execute(); } /* Build the result. */ - ObjectFactory objectFactory = new ObjectFactory(); - for (Object object : queryResult) { - Object[] fields = (Object[]) object; - - String spaceId = (String) fields[1]; - List<String> spaces = Utils.getSpacesFromSpaceId(spaceId); - String pageName = (String) fields[2]; - String className = (String) fields[3]; - int objectNumber = (Integer) fields[4]; - - String id = Utils.getObjectId(options.wikiName(), spaces, pageName, className, objectNumber); - - String pageId = Utils.getPageId(options.wikiName(), spaces, pageName); - String pageFullName = Utils.getPageFullName(options.wikiName(), spaces, pageName); - - /* - * Check if the user has the right to see the found document. We also prevent guest users to access - * object data in order to avoid leaking important information such as emails to crawlers. - */ - if (xwikiApi.hasAccessLevel("view", pageId) && context.getUserReference() != null) { - Document doc = xwikiApi.getDocument(pageFullName); - String title = doc.getDisplayTitle(); - SearchResult searchResult = objectFactory.createSearchResult(); - searchResult.setType("object"); - searchResult.setId(id); - searchResult.setPageFullName(pageFullName); - searchResult.setTitle(title); - searchResult.setWiki(options.wikiName()); - searchResult.setSpace(spaceId); - searchResult.setPageName(pageName); - searchResult.setVersion(doc.getVersion()); - searchResult.setClassName(className); - searchResult.setObjectNumber(objectNumber); - searchResult.setAuthor(doc.getAuthor()); - Calendar calendar = Calendar.getInstance(); - calendar.setTime(doc.getDate()); - searchResult.setModified(calendar); - - if (options.withPrettyNames()) { - searchResult.setAuthorName( - context.getWiki().getPlainUserName(doc.getAuthorReference(), context)); - } - - List<String> restSpacesValue = Utils.getSpacesURLElements(spaces); - - String pageUri = - Utils.createURI(baseURI, PageResource.class, options.wikiName(), restSpacesValue, pageName) - .toString(); - Link pageLink = new Link(); - pageLink.setHref(pageUri); - pageLink.setRel(Relations.PAGE); - searchResult.getLinks().add(pageLink); + return searchObjects(options, baseURI, queryResult, context); + } finally { + this.contextProvider.get().setWikiId(database); + } + } + + private List<SearchResult> searchObjects(KeywordSearchOptions options, URI baseURI, List<Object> queryResult, + XWikiContext context) throws UriBuilderException, XWikiException + { + List<SearchResult> result = new ArrayList<>(); + + XWiki xwikiApi = new XWiki(context.getWiki(), context); + + /* Build the result. */ + ObjectFactory objectFactory = new ObjectFactory(); + for (Object object : queryResult) { + Object[] fields = (Object[]) object; + + String spaceId = (String) fields[1]; + List<String> spaces = Utils.getSpacesFromSpaceId(spaceId); + String pageName = (String) fields[2]; + String className = (String) fields[3]; + int objectNumber = (Integer) fields[4]; - String objectUri = Utils.createURI(baseURI, ObjectResource.class, options.wikiName(), - restSpacesValue, pageName, className, objectNumber).toString(); - Link objectLink = new Link(); - objectLink.setHref(objectUri); - objectLink.setRel(Relations.OBJECT); - searchResult.getLinks().add(objectLink); + String id = Utils.getObjectId(options.wikiName(), spaces, pageName, className, objectNumber); - result.add(searchResult); + String pageId = Utils.getPageId(options.wikiName(), spaces, pageName); + String pageFullName = Utils.getPageFullName(options.wikiName(), spaces, pageName); + + /* + * Check if the user has the right to see the found document. We also prevent guest users to access object + * data in order to avoid leaking important information such as emails to crawlers. + */ + if (xwikiApi.hasAccessLevel("view", pageId) && context.getUserReference() != null) { + Document doc = xwikiApi.getDocument(pageFullName); + String title = doc.getDisplayTitle(); + SearchResult searchResult = objectFactory.createSearchResult(); + searchResult.setType("object"); + searchResult.setId(id); + searchResult.setPageFullName(pageFullName); + searchResult.setTitle(title); + searchResult.setWiki(options.wikiName()); + searchResult.setSpace(spaceId); + searchResult.setPageName(pageName); + searchResult.setVersion(doc.getVersion()); + searchResult.setClassName(className); + searchResult.setObjectNumber(objectNumber); + searchResult.setAuthor(doc.getAuthor()); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(doc.getDate()); + searchResult.setModified(calendar); + + if (options.withPrettyNames()) { + searchResult.setAuthorName(context.getWiki().getPlainUserName(doc.getAuthorReference(), context)); } - } - return result; - } finally { - this.contextProvider.get().setWikiId(database); + List<String> restSpacesValue = Utils.getSpacesURLElements(spaces); + + String pageUri = Utils + .createURI(baseURI, PageResource.class, options.wikiName(), restSpacesValue, pageName).toString(); + Link pageLink = new Link(); + pageLink.setHref(pageUri); + pageLink.setRel(Relations.PAGE); + searchResult.getLinks().add(pageLink); + + String objectUri = Utils.createURI(baseURI, ObjectResource.class, options.wikiName(), restSpacesValue, + pageName, className, objectNumber).toString(); + Link objectLink = new Link(); + objectLink.setHref(objectUri); + objectLink.setRel(Relations.OBJECT); + searchResult.getLinks().add(objectLink); + + result.add(searchResult); + } } + + return result; } }
743ebf8696ffXWIKI-23247: Improve REST search query execution
6 files changed · +62 −20
xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/store/hibernate/query/HqlQueryUtils.java+20 −0 modified@@ -19,6 +19,7 @@ */ package com.xpn.xwiki.internal.store.hibernate.query; +import java.util.Set; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -47,6 +48,8 @@ public final class HqlQueryUtils private static final Pattern LEGACY_ORDINAL_PARAMS_PATTERN = Pattern.compile("([=\\s,\\(<>])\\?([=\\s,\\)<>]|$)"); + private static final Set<String> VALID_ORDER = Set.of("asc", "desc"); + private HqlQueryUtils() { @@ -203,4 +206,21 @@ public static String getColumnsForSelectStatement(String whereSQL) return columns.toString(); } + + /** + * @param order the order + * @param def the default value to return if the value is not valid + * @return the valid value + * @since 17.5.0RC1 + * @since 17.4.2 + * @since 16.10.9 + */ + public static String getValidQueryOrder(String order, String def) + { + if (order == null || !VALID_ORDER.contains(order.toLowerCase())) { + return def; + } + + return order; + } }
xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/internal/store/hibernate/query/HqlQueryUtilsTest.java+12 −0 modified@@ -65,6 +65,18 @@ public void toCompleteStatement() HqlQueryUtils.toCompleteStatement("order by doc.name")); assertEquals("select doc.fullName from XWikiDocument doc , XWikiSpace space", HqlQueryUtils.toCompleteStatement(", XWikiSpace space")); + } + + @Test + public void getValidQueryOrder() + { + assertEquals("asc", HqlQueryUtils.getValidQueryOrder("asc", "desc")); + assertEquals("desc", HqlQueryUtils.getValidQueryOrder("desc", "asc")); + assertEquals("ASC", HqlQueryUtils.getValidQueryOrder("ASC", "desc")); + assertEquals("DESC", HqlQueryUtils.getValidQueryOrder("DESC", "asc")); + assertEquals("desc", HqlQueryUtils.getValidQueryOrder(null, "desc")); + assertEquals("desc", HqlQueryUtils.getValidQueryOrder("wrong", "desc")); + assertEquals("asc", HqlQueryUtils.getValidQueryOrder("wrong", "asc")); } }
xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/main/java/org/xwiki/rest/internal/resources/BaseSearchResult.java+11 −10 modified@@ -40,6 +40,7 @@ import org.xwiki.query.Query; import org.xwiki.query.QueryException; import org.xwiki.query.QueryFilter; +import org.xwiki.query.QueryManager; import org.xwiki.rest.Relations; import org.xwiki.rest.XWikiResource; import org.xwiki.rest.internal.ModelFactory; @@ -58,6 +59,7 @@ import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.api.Document; import com.xpn.xwiki.api.XWiki; +import com.xpn.xwiki.internal.store.hibernate.query.HqlQueryUtils; /** * @version $Id$ @@ -95,6 +97,10 @@ protected enum SearchScope @Inject private EntityReferenceProvider defaultEntityReferenceProvider; + @Inject + @Named("secure") + private QueryManager secureQueryManager; + /** * Search for keyword in the given scopes. See {@link SearchScope} for more information. * @@ -243,19 +249,14 @@ protected List<SearchResult> searchPages(List<SearchScope> searchScopes, String if (StringUtils.isBlank(orderField)) { orderClause = "doc.fullName asc"; } else { - /* Check if the order parameter is a valid "asc" or "desc" string, otherwise use "asc" */ - if ("asc".equals(order) || "desc".equals(order)) { - orderClause = String.format("doc.%s %s", orderField, order); - } else { - orderClause = String.format("doc.%s asc", orderField); - } + orderClause = String.format("doc.%s %s", orderField, HqlQueryUtils.getValidQueryOrder(order, "asc")); } // Add ordering f.format(") order by %s", orderClause); String queryString = f.toString(); - Query query = this.queryManager.createQuery(queryString, Query.HQL) + Query query = this.secureQueryManager.createQuery(queryString, Query.HQL) .bindValue("keywords", String.format("%%%s%%", keywords.toUpperCase())) .addFilter(Utils.getHiddenQueryFilter(this.componentManager)).setOffset(start) // Worst case scenario when making the locale aware query: @@ -393,7 +394,7 @@ protected List<SearchResult> searchSpaces(String keywords, String wikiName, int + " or lower(space.reference) like lower(:prefix) escape '!'" + " order by lower(space.reference), space.reference"; - List<Object> queryResult = queryManager.createQuery(query, Query.HQL) + List<Object> queryResult = this.secureQueryManager.createQuery(query, Query.HQL) .bindValue("keywords", String.format("%%%s%%", escapedKeywords)) .bindValue("prefix", String.format("%s%%", escapedKeywords)) .setWiki(wikiName).setLimit(number).setOffset(start) @@ -516,12 +517,12 @@ protected List<SearchResult> searchObjects(String keywords, String wikiName, Str /* This is needed because if the :space placeholder is not in the query, setting it would cause an exception */ if (space != null) { queryResult = - queryManager.createQuery(query, Query.XWQL) + this.secureQueryManager.createQuery(query, Query.XWQL) .bindValue("keywords", String.format("%%%s%%", keywords.toUpperCase())) .bindValue("space", space).setLimit(number).execute(); } else { queryResult = - queryManager.createQuery(query, Query.XWQL) + this.secureQueryManager.createQuery(query, Query.XWQL) .bindValue("keywords", String.format("%%%s%%", keywords.toUpperCase())).setLimit(number) .execute(); }
xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/main/java/org/xwiki/rest/internal/resources/ModificationsResourceImpl.java+6 −3 modified@@ -41,6 +41,7 @@ import org.xwiki.security.authorization.ContextualAuthorizationManager; import com.xpn.xwiki.doc.rcs.XWikiRCSNodeId; +import com.xpn.xwiki.internal.store.hibernate.query.HqlQueryUtils; import static org.xwiki.security.authorization.Right.VIEW; @@ -62,16 +63,18 @@ public History getModifications(String wikiName, Integer start, Integer number, Boolean withPrettyNames) throws XWikiRestException { try { + String validOrder = HqlQueryUtils.getValidQueryOrder(order, "desc"); + History history = new History(); String query = String.format("select doc.space, doc.name, doc.language, rcs.id, rcs.date, rcs.author," + " rcs.comment from XWikiRCSNodeInfo as rcs, XWikiDocument as doc where rcs.id.docId = doc.id and" + " rcs.date > :date order by rcs.date %s, rcs.id.version1 %s, rcs.id.version2 %s", - order, order, order); + validOrder, validOrder, validOrder); List<Object> queryResult = null; - queryResult = queryManager.createQuery(query, Query.XWQL).bindValue("date", new Date(ts)).setLimit(number) - .setOffset(start).setWiki(wikiName).execute(); + queryResult = this.queryManager.createQuery(query, Query.XWQL).bindValue("date", new Date(ts)) + .setLimit(number).setOffset(start).setWiki(wikiName).execute(); for (Object object : queryResult) { Object[] fields = (Object[]) object;
xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/main/java/org/xwiki/rest/internal/resources/pages/PageHistoryResourceImpl.java+6 −3 modified@@ -44,6 +44,7 @@ import org.xwiki.security.authorization.Right; import com.xpn.xwiki.doc.rcs.XWikiRCSNodeId; +import com.xpn.xwiki.internal.store.hibernate.query.HqlQueryUtils; /** * @version $Id$ @@ -73,15 +74,17 @@ public History getPageHistory(String wikiName, String spaceName, String pageName History history = new History(); try { + String validOrder = HqlQueryUtils.getValidQueryOrder(order, "desc"); + // Note that the query is made to work with Oracle which treats empty strings as null. String query = String.format("select doc.space, doc.name, rcs.id, rcs.date, rcs.author, rcs.comment" + " from XWikiRCSNodeInfo as rcs, XWikiDocument as doc where rcs.id.docId = doc.id and" + " doc.space = :space and doc.name = :name and (doc.language = '' or doc.language is null)" - + " order by rcs.date %s, rcs.id.version1 %s, rcs.id.version2 %s", order, order, order); + + " order by rcs.date %s, rcs.id.version1 %s, rcs.id.version2 %s", validOrder, validOrder, validOrder); List<Object> queryResult = null; - queryResult = queryManager.createQuery(query, Query.XWQL).bindValue("space", spaceId).bindValue("name", - pageName).setLimit(number).setOffset(start).setWiki(wikiName).execute(); + queryResult = this.queryManager.createQuery(query, Query.XWQL).bindValue("space", spaceId) + .bindValue("name", pageName).setLimit(number).setOffset(start).setWiki(wikiName).execute(); for (Object object : queryResult) { Object[] fields = (Object[]) object;
xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/main/java/org/xwiki/rest/internal/resources/pages/PageTranslationHistoryResourceImpl.java+7 −4 modified@@ -44,6 +44,7 @@ import org.xwiki.security.authorization.Right; import com.xpn.xwiki.doc.rcs.XWikiRCSNodeId; +import com.xpn.xwiki.internal.store.hibernate.query.HqlQueryUtils; /** * @version $Id$ @@ -73,15 +74,17 @@ public History getPageTranslationHistory(String wikiName, String spaceName, Stri History history = new History(); try { + String validOrder = HqlQueryUtils.getValidQueryOrder(order, "desc"); + String query = String.format("select doc.space, doc.name, rcs.id, rcs.date, rcs.author, rcs.comment" + " from XWikiRCSNodeInfo as rcs, XWikiDocument as doc where rcs.id.docId = doc.id and" + " doc.space = :space and doc.name = :name and doc.language = :language" - + " order by rcs.date %s, rcs.id.version1 %s, rcs.id.version2 %s", order, order, order); + + " order by rcs.date %s, rcs.id.version1 %s, rcs.id.version2 %s", validOrder, validOrder, validOrder); List<Object> queryResult = null; - queryResult = queryManager.createQuery(query, Query.XWQL).bindValue("space", spaceId).bindValue("name", - pageName).setLimit(number).bindValue("language", language).setOffset(start) - .setWiki(wikiName).execute(); + queryResult = this.queryManager.createQuery(query, Query.XWQL).bindValue("space", spaceId) + .bindValue("name", pageName).setLimit(number).bindValue("language", language).setOffset(start) + .setWiki(wikiName).execute(); for (Object object : queryResult) { Object[] fields = (Object[]) object;
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-gprp-h92g-gc2hghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-52472ghsaADVISORY
- github.com/xwiki/xwiki-platform/commit/743ebf8696ffa55161ed2c5ecf26b09f69e6bcf1nvdWEB
- github.com/xwiki/xwiki-platform/commit/a45eca2af772abb7324e56d7fd2df1ac937bc445nvdWEB
- github.com/xwiki/xwiki-platform/security/advisories/GHSA-gprp-h92g-gc2hnvdWEB
- jira.xwiki.org/browse/XWIKI-23247nvdWEB
News mentions
0No linked articles in our index yet.