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:
parent
72171c809f
commit
f00ea5f790
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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("'", "''");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue