Fixes Ickle injection issue (#5856)

Updates to Infinispan 16.0.14
Covers Ickle query injection issue

---------

Co-authored-by: Katia Aresti <karestig@ibm.com>
Co-authored-by: Dmytro Liubarskyi <ljubarskij@gmail.com>
This commit is contained in:
Katia Aresti 2026-07-29 11:58:36 +02:00 committed by GitHub
parent 72171c809f
commit f00ea5f790
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 192 additions and 45 deletions

View File

@ -13,8 +13,8 @@
<name>LangChain4j :: Integration :: Infinispan</name>
<properties>
<infinispan.version>16.0.6</infinispan.version>
<protostream.version>6.0.6</protostream.version>
<infinispan.version>16.0.14</infinispan.version>
<protostream.version>6.0.7</protostream.version>
</properties>
<dependencyManagement>
@ -26,6 +26,11 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.6.3.Final</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -14,7 +14,6 @@ import dev.langchain4j.store.embedding.filter.logical.And;
import dev.langchain4j.store.embedding.filter.logical.Not;
import dev.langchain4j.store.embedding.filter.logical.Or;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Collectors;
/**
@ -107,48 +106,38 @@ class InfinispanMetadataFilterMapper {
}
private String mapIn(IsIn filter) {
Optional<?> first = filter.comparisonValues().stream().findFirst();
if (first.isEmpty()) {
throw new UnsupportedOperationException("Infinispan metadata filter IN must contain values");
}
Object o = first.get();
String inStatement = formattedComparisonValues(filter.comparisonValues(), o instanceof Number);
String m = "m" + i + ".";
String filterQuery = m + "value IN (" + inStatement + ")";
if (o instanceof Integer || o instanceof Long) {
filterQuery = m + "value_int IN (" + inStatement + ")";
} else if (o instanceof Float || o instanceof Double) {
filterQuery = m + "value_float IN (" + inStatement + ")";
}
ensureNotEmpty(filter.comparisonValues());
String column = "m" + i + "." + valueColumn(filter.comparisonValues());
String inStatement = formattedComparisonValues(filter.comparisonValues());
return metadataKey(filter.key()) + filterQuery;
return metadataKey(filter.key()) + column + " IN (" + inStatement + ")";
}
private String mapNotIn(IsNotIn filter) {
Optional<?> first = filter.comparisonValues().stream().findFirst();
if (first.isEmpty()) {
ensureNotEmpty(filter.comparisonValues());
String m = "m" + i + ".";
String column = m + valueColumn(filter.comparisonValues());
String inStatement = formattedComparisonValues(filter.comparisonValues());
return "(" + column + " NOT IN (" + inStatement + ")" + metadataKeyLast(filter.key()) + ") " + "OR ("
+ column + " IN (" + inStatement + ") and " + m + "name!='" + escape(filter.key()) + "')"
+ addMetadataNullCheck();
}
private void ensureNotEmpty(Collection<?> comparisonValues) {
if (comparisonValues.isEmpty()) {
throw new UnsupportedOperationException("Infinispan metadata filter IN must contain values");
}
Object o = first.get();
String inStatement = formattedComparisonValues(filter.comparisonValues(), o instanceof Number);
String m = "m" + i + ".";
String filterQuery = m + "value NOT IN (" + inStatement + ")";
if (o instanceof Integer || o instanceof Long) {
filterQuery = m + "value_int NOT IN (" + inStatement + ")";
} else if (o instanceof Float || o instanceof Double) {
filterQuery = m + "value_float NOT IN (" + inStatement + ")";
}
}
String inFilterQuery = m + "value IN (" + inStatement + ")";
if (o instanceof Integer || o instanceof Long) {
inFilterQuery = m + "value_int IN (" + inStatement + ")";
} else if (o instanceof Float || o instanceof Double) {
inFilterQuery = m + "value_float IN (" + inStatement + ")";
private String valueColumn(Collection<?> comparisonValues) {
if (comparisonValues.stream().anyMatch(v -> v instanceof Float || v instanceof Double)) {
return "value_float";
}
return "(" + filterQuery + metadataKeyLast(filter.key()) + ") " + "OR ("
+ inFilterQuery + " and " + m + "name!='" + escape(filter.key()) + "')"
+ addMetadataNullCheck();
if (comparisonValues.stream().allMatch(v -> v instanceof Integer || v instanceof Long)) {
return "value_int";
}
return "value";
}
private String computeFilter(String operator, Object value) {
@ -198,14 +187,30 @@ class InfinispanMetadataFilterMapper {
return "m" + i + ".name='" + escape(key) + "' and ";
}
private String formattedComparisonValues(Collection<?> comparisonValues, boolean isNumeric) {
String inStatement = comparisonValues.stream()
.map(s -> isNumeric ? s.toString() : "'" + escape(String.valueOf(s)) + "'")
private String formattedComparisonValues(Collection<?> comparisonValues) {
boolean hasNumeric = comparisonValues.stream().anyMatch(v -> v instanceof Number);
boolean hasNonNumeric = comparisonValues.stream().anyMatch(v -> !(v instanceof Number));
if (hasNumeric && hasNonNumeric) {
throw new IllegalArgumentException(
"Infinispan metadata filter IN/NOT IN cannot mix numeric and non-numeric values");
}
boolean asFloat = "value_float".equals(valueColumn(comparisonValues));
return comparisonValues.stream()
.map(s -> formattedComparisonValue(s, asFloat))
.collect(Collectors.joining(", "));
return inStatement;
}
private String formattedComparisonValue(Object value, boolean asFloat) {
if (!(value instanceof Number)) {
return "'" + escape(String.valueOf(value)) + "'";
}
if (asFloat && (value instanceof Integer || value instanceof Long)) {
return String.valueOf(((Number) value).doubleValue());
}
return value.toString();
}
private static String escape(String s) {
return s.replace("'", "''");
return s.replace("\\", "\\\\").replace("'", "''");
}
}

View File

@ -369,15 +369,68 @@ class InfinispanMetadataFilterMapperTest {
@Test
void should_handle_mixed_numeric_types_in_in_filter() {
// given
// given all values are Number (different subtypes), no mixing with strings
Filter filter = new IsIn("mixed", Arrays.asList(1, 2L, 3.0f, 4.0));
// when
InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
// then presence of a floating point value selects value_float for the whole list,
// and integral values are widened to match the column
assertThat(result.query).isEqualTo("m0.name='mixed' and m0.value_float IN (3.0, 4.0, 1.0, 2.0)");
}
@ParameterizedTest
@MethodSource("numericInColumnSelection")
void should_select_in_column_independently_of_iteration_order(List<?> values, String expectedColumn) {
// when
InfinispanMetadataFilterMapper.FilterResult result = mapper.map(new IsIn("n", values));
// then
// Should use the type of the first element (Integer in this case)
assertThat(result.query).isEqualTo("m0.name='mixed' and m0.value_float IN (3.0, 4.0, 1, 2)");
assertThat(result.query).startsWith("m0.name='n' and m0." + expectedColumn + " IN (");
}
static List<Arguments> numericInColumnSelection() {
return Arrays.asList(
Arguments.of(Arrays.asList(1, 2, 3), "value_int"),
Arguments.of(Arrays.asList(1, 2L), "value_int"),
Arguments.of(Arrays.asList(1.5, 2.5), "value_float"),
// these two pairs hash into opposite iteration orders; both must pick value_float
Arguments.of(Arrays.asList(1, 2.5), "value_float"),
Arguments.of(Arrays.asList(3, 0.5), "value_float"),
Arguments.of(Arrays.asList("a", "b"), "value"));
}
@Test
void should_select_not_in_column_independently_of_iteration_order() {
// when
InfinispanMetadataFilterMapper.FilterResult result = mapper.map(new IsNotIn("n", Arrays.asList(1, 2.5)));
// then both occurrences of the column must agree
assertThat(result.query).contains("m0.value_float NOT IN (").contains("m0.value_float IN (");
assertThat(result.query).doesNotContain("value_int");
}
@Test
void should_reject_mixed_numeric_and_string_in_filter() {
// given mix of Number and String triggers rejection regardless of iteration order
Filter filter = new IsIn("key", Arrays.asList(1, "x' OR 1=1 --"));
// when & then
assertThatThrownBy(() -> mapper.map(filter))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("cannot mix numeric and non-numeric values");
}
@Test
void should_reject_mixed_numeric_and_string_not_in_filter() {
// given
Filter filter = new IsNotIn("key", Arrays.asList(42, "<ickle fragment>"));
// when & then
assertThatThrownBy(() -> mapper.map(filter))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("cannot mix numeric and non-numeric values");
}
@Test
@ -441,6 +494,90 @@ class InfinispanMetadataFilterMapperTest {
assertThat(result.query).contains("OR (i.metadata is null)");
}
@Test
void should_escape_ickle_injection_in_key() {
// given
Filter filter = new IsEqualTo("foo' OR 1=1 OR name='", "bar");
// when
InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
// then
assertThat(result.query).isEqualTo("m0.name='foo'' OR 1=1 OR name=''' and m0.value = 'bar'");
}
@Test
void should_escape_ickle_injection_in_value() {
// given
Filter filter = new IsEqualTo("name", "x' OR 1=1 --");
// when
InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
// then
assertThat(result.query).isEqualTo("m0.name='name' and m0.value = 'x'' OR 1=1 --'");
}
@Test
void should_escape_ickle_injection_in_not_equal_key() {
// given
Filter filter = new IsNotEqualTo("a' OR 1=1 --", "val");
// when
InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
// then
assertThat(result.query).contains("m0.name='a'' OR 1=1 --'");
}
@Test
void should_escape_ickle_injection_in_in_key() {
// given
Filter filter = new IsIn("k' OR 1=1 --", Arrays.asList("a", "b"));
// when
InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
// then
assertThat(result.query).contains("m0.name='k'' OR 1=1 --'");
}
@Test
void should_escape_ickle_injection_in_in_values() {
// given
Filter filter = new IsIn("key", Arrays.asList("x' OR 1=1 --", "normal"));
// when
InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
// then
assertThat(result.query).contains("'x'' OR 1=1 --'");
}
@Test
void should_escape_ickle_injection_in_not_in_key() {
// given
Filter filter = new IsNotIn("k' OR 1=1 --", Arrays.asList("a"));
// when
InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
// then
assertThat(result.query).contains("m0.name='k'' OR 1=1 --'").contains("m0.name!='k'' OR 1=1 --'");
}
@Test
void should_escape_backslash_in_value() {
// given
Filter filter = new IsEqualTo("path", "C:\\Users\\test");
// when
InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
// then
assertThat(result.query).isEqualTo("m0.name='path' and m0.value = 'C:\\\\Users\\\\test'");
}
@Test
void should_generate_correct_metadata_aliases_for_nested_filters() {
// given