Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions redis-plugin/docker-compose-sentinel.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -541,14 +548,23 @@ 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;
}
if (filter.isAttributeFilter()) {
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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<String> 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<String> filter =
new SolrSearchEngineFilter<>("attr) OR (*:*", true, "value");

assertThrows(IllegalArgumentException.class, () ->
searcherDAO.searchFacetedContents(new SearchEngineFilter[]{filter},
new SearchEngineFilter[]{}, new ArrayList<>()));
}

@Test
void shouldRejectFieldNameWithSpaces() {
SolrSearchEngineFilter<String> 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<String> 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<String> 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<String> 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<String> 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)
// ------------------------------------------------------------------
Expand Down
Loading