From 28332d01ec6b56fb4352e323ac1fde5a3e06596f Mon Sep 17 00:00:00 2001 From: "Matteo E. Minnai" Date: Fri, 26 Jun 2026 14:12:42 +0200 Subject: [PATCH] ESB-1154 fix Lucene query injection via unsanitized field names --- .../jpsolr/aps/system/solr/SearcherDAO.java | 18 +++- .../solr/SearcherDAOSpecialCharsTest.java | 95 +++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) 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 ad46976d4..fb5a799ec 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; @@ -541,6 +548,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()) { + throw new IllegalArgumentException( + "Rejected Solr field key with unsafe characters: '" + key + "'"); + } if (filter.isFullTextSearch()) { return key; } @@ -548,7 +559,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()) { + throw new IllegalArgumentException( + "Rejected Solr lang code with unsafe characters: '" + normalizedLang + "'"); + } + key = normalizedLang + "_" + key; } else if (!key.startsWith(SolrFields.SOLR_FIELD_PREFIX)) { key = SolrFields.SOLR_FIELD_PREFIX + key; } diff --git a/solr-plugin/src/test/java/org/entando/entando/plugins/jpsolr/aps/system/solr/SearcherDAOSpecialCharsTest.java b/solr-plugin/src/test/java/org/entando/entando/plugins/jpsolr/aps/system/solr/SearcherDAOSpecialCharsTest.java index fdfcd195d..e9ac539c9 100644 --- a/solr-plugin/src/test/java/org/entando/entando/plugins/jpsolr/aps/system/solr/SearcherDAOSpecialCharsTest.java +++ b/solr-plugin/src/test/java/org/entando/entando/plugins/jpsolr/aps/system/solr/SearcherDAOSpecialCharsTest.java @@ -21,6 +21,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.assertThrows; /** * Special-character handling tests for SearcherDAO query building. @@ -243,6 +244,100 @@ void exactPhraseMultipleValuesEmitsQuotedOutput_documentingCurrentBehavior() thr "+(entity_key:\"foo:bar\" entity_key:\"baz+qux\") +(entity_group:free)"); } + // ------------------------------------------------------------------ + // Field-key injection tests (CVE-class: Lucene query injection) + // + // User-controlled field names that contain Lucene/Solr meta-characters + // (parentheses, spaces, operators, local-params braces …) corrupt the + // query string produced by BooleanQuery.toString() and can break the + // group-based access-control filter. All such inputs must be rejected + // with IllegalArgumentException before any Term object is constructed. + // ------------------------------------------------------------------ + + @Test + void shouldRejectFieldNameWithParenthesisAndOrOperator() { + // Simulates: filters[0].attribute = "foo) OR (*:*" + SolrSearchEngineFilter filter = + new SolrSearchEngineFilter<>("foo) OR (*:*", false, "value"); + + assertThrows(IllegalArgumentException.class, () -> + searcherDAO.searchFacetedContents(new SearchEngineFilter[]{filter}, + new SearchEngineFilter[]{}, new ArrayList<>())); + } + + @Test + void shouldRejectEntityAttrWithInjectedOperator() { + // Simulates: filters[0].entityAttr = "attr) OR (*:*" (isAttributeFilter = true) + SolrSearchEngineFilter filter = + new SolrSearchEngineFilter<>("attr) OR (*:*", true, "value"); + + assertThrows(IllegalArgumentException.class, () -> + searcherDAO.searchFacetedContents(new SearchEngineFilter[]{filter}, + new SearchEngineFilter[]{}, new ArrayList<>())); + } + + @Test + void shouldRejectFieldNameWithSpaces() { + SolrSearchEngineFilter filter = + new SolrSearchEngineFilter<>("valid AND malicious", false, "value"); + + assertThrows(IllegalArgumentException.class, () -> + searcherDAO.searchFacetedContents(new SearchEngineFilter[]{filter}, + new SearchEngineFilter[]{}, new ArrayList<>())); + } + + @Test + void shouldRejectLangCodeUsedAsFullTextSearchKey() { + // Simulates: lang = "en) OR (*:*" passed as the full-text search field key. + // The filter key IS the lang code when fullTextSearch = true. + SolrSearchEngineFilter filter = + new SolrSearchEngineFilter<>("en) OR (*:*", "search text", + TextSearchOption.AT_LEAST_ONE_WORD); + filter.setFullTextSearch(true); + + assertThrows(IllegalArgumentException.class, () -> + searcherDAO.searchFacetedContents(new SearchEngineFilter[]{filter}, + new SearchEngineFilter[]{}, new ArrayList<>())); + } + + @Test + void shouldRejectInjectedLangCodePrefixOnAttributeFilter() { + // Simulates a crafted SolrSearchEngineFilter whose langCode (the prefix + // prepended to the field name for attribute filters) contains operators. + SolrSearchEngineFilter filter = + new SolrSearchEngineFilter<>("key", true, "value"); + filter.setLangCode("en) OR (*:*"); + + assertThrows(IllegalArgumentException.class, () -> + searcherDAO.searchFacetedContents(new SearchEngineFilter[]{filter}, + new SearchEngineFilter[]{}, new ArrayList<>())); + } + + @Test + void shouldRejectFieldNameWithSolrLocalParamsBrace() { + // Simulates an attempt to inject Solr local params ({!...}) via field name. + SolrSearchEngineFilter filter = + new SolrSearchEngineFilter<>("{!func}log(popularity)", false, "value"); + + assertThrows(IllegalArgumentException.class, () -> + searcherDAO.searchFacetedContents(new SearchEngineFilter[]{filter}, + new SearchEngineFilter[]{}, new ArrayList<>())); + } + + @Test + void shouldRejectFieldNameInDoubleFilterArray() { + // Same injection via the searchFacetedContents(SearchEngineFilter[][] …) overload. + SolrSearchEngineFilter filter = + new SolrSearchEngineFilter<>("foo) OR (*:*", false, "value"); + + SearchEngineFilter[][] doubleFilters = + new SearchEngineFilter[][]{{filter}}; + + assertThrows(IllegalArgumentException.class, () -> + searcherDAO.searchFacetedContents(doubleFilters, + new SearchEngineFilter[]{}, new ArrayList<>())); + } + // ------------------------------------------------------------------ // helpers (mirror SearcherDAOTest) // ------------------------------------------------------------------