-
Notifications
You must be signed in to change notification settings - Fork 7
[NAE-2310] Elasticsearch fulltext query input sanitation #401
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+483
−16
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
fbfa791
[NAE-2310] Elasticsearch fulltext query input sanitation
tuplle ed2a1b6
[NAE-2310] Elasticsearch fulltext query input sanitation
tuplle 43d3a78
[NAE-2310] Elasticsearch fulltext query input sanitation
tuplle 17bf9a0
[NAE-2310] Elasticsearch fulltext query input sanitation
tuplle df0c11e
[NAE-2310] Elasticsearch fulltext query input sanitation
tuplle 5f95ede
[NAE-2310] Elasticsearch fulltext query input sanitation
tuplle 5fa7cef
[NAE-2310] Elasticsearch fulltext query input sanitation
tuplle 1c70c61
Merge remote-tracking branch 'origin/release/6.4.2' into NAE-2310
tuplle File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
98 changes: 98 additions & 0 deletions
98
...main/java/com/netgrif/application/engine/elastic/service/ElasticsearchQuerySanitizer.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| package com.netgrif.application.engine.elastic.service; | ||
|
|
||
|
|
||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.apache.commons.lang.StringUtils; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import java.util.stream.Collectors; | ||
|
|
||
|
|
||
| /** | ||
| * The ElasticsearchQuerySanitizer class is responsible for sanitizing Elasticsearch queries | ||
| * by escaping or removing reserved characters and keywords. This is essential to ensure proper | ||
| * handling of Elasticsearch queries and to prevent syntax issues caused by special characters or | ||
| * reserved words. | ||
| * <p> | ||
| * This class provides utility methods to sanitize query strings by escaping predefined reserved | ||
| * characters, removing certain reserved characters, and excluding specific keywords if provided. | ||
| * The reserved characters and keywords are predefined and managed internally. | ||
| */ | ||
| @Slf4j | ||
| public class ElasticsearchQuerySanitizer { | ||
|
|
||
| public static final String[] RESERVED_CHARACTERS_TO_ESCAPE = {"\\", "+", "-", "=", "&&", "||", "!", "(", ")", "{", "}", "[", "]", "^", "\"", "~", "*", "?", ":", "/", "AND", "OR", "NOT", " "}; | ||
| public static final String[] RESERVED_CHARACTERS_TO_REMOVE = {">", "<"}; | ||
| public static final Map<String, String> RESERVED_KEYWORDS = prepareReservedKeywords(); | ||
|
|
||
| /** | ||
| * Sanitizes the provided Elasticsearch query string by escaping or removing certain reserved | ||
| * characters and excluding specific keywords if applicable. | ||
| * <p> | ||
| * This method applies default sanitization rules and does not consider keyword exclusions. | ||
| * | ||
| * @param query the Elasticsearch query string to sanitize, such as a search query or filter. | ||
| * It must not be null to ensure proper sanitization. | ||
| * @return the sanitized query string with reserved characters handled appropriately. | ||
| * If the input is empty or null, the behavior depends on the implemented sanitization logic. | ||
| */ | ||
| public static String sanitize(String query) { | ||
| return sanitize(query, null); | ||
| } | ||
|
|
||
| /** | ||
| * Sanitizes the given query string by replacing reserved keywords with their sanitized equivalents, | ||
| * excluding the specified keywords from sanitization. | ||
| * | ||
| * @param query the query string to sanitize, which may contain reserved characters and keywords. | ||
| * This string must not be null. | ||
| * @param exclude an array of keywords to exclude from sanitization. If null or empty, all reserved | ||
| * keywords will be considered for sanitization. | ||
| * @return the sanitized query string with reserved keywords appropriately replaced, and excluded | ||
| * keywords untouched. | ||
| */ | ||
| public static String sanitize(String query, String[] exclude) { | ||
| if (query == null || query.isBlank()) { | ||
| return query; | ||
| } | ||
| Map<String, String> keywordsToEscape = excludeKeywords(exclude); | ||
| String sanitized = StringUtils.replaceEach(query, | ||
| keywordsToEscape.keySet().toArray(new String[0]), | ||
| keywordsToEscape.values().toArray(new String[0])); | ||
| log.trace("Sanitized query: {}", sanitized); | ||
| return sanitized; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| protected static Map<String, String> prepareReservedKeywords() { | ||
| Map<String, String> result = new HashMap<>(); | ||
| for (String reservedString : RESERVED_CHARACTERS_TO_ESCAPE) { | ||
| String escaped = Arrays.stream(reservedString.split("")) | ||
|
tuplle marked this conversation as resolved.
|
||
| .map(c -> "\\" + c) | ||
| .collect(Collectors.joining("")); | ||
| result.put(reservedString, escaped); | ||
| } | ||
| for (String reservedString : RESERVED_CHARACTERS_TO_REMOVE) { | ||
| result.put(reservedString, "\\ "); | ||
|
tuplle marked this conversation as resolved.
|
||
| } | ||
|
|
||
| return Collections.unmodifiableMap(result); | ||
| } | ||
|
|
||
| protected static Map<String, String> excludeKeywords(String[] exclude) { | ||
| if (exclude == null || exclude.length == 0) { | ||
| return RESERVED_KEYWORDS; | ||
| } | ||
| Map<String, String> keywordsToEscape = new HashMap<>(RESERVED_KEYWORDS); | ||
| for (String toExclude : exclude) { | ||
| if (RESERVED_KEYWORDS.containsKey(toExclude)) { | ||
| keywordsToEscape.remove(toExclude); | ||
| } | ||
| } | ||
| return Collections.unmodifiableMap(keywordsToEscape); | ||
| } | ||
|
|
||
|
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
...grif/application/engine/workflow/utils/CaseSearchRequestSingleItemAsListDeserializer.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| package com.netgrif.application.engine.workflow.utils; | ||
|
|
||
| import com.fasterxml.jackson.core.JsonParser; | ||
| import com.fasterxml.jackson.databind.BeanProperty; | ||
| import com.fasterxml.jackson.databind.DeserializationContext; | ||
| import com.fasterxml.jackson.databind.JsonDeserializer; | ||
| import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; | ||
| import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; | ||
| import com.netgrif.application.engine.elastic.web.requestbodies.singleaslist.SingleCaseSearchRequestAsList; | ||
| import com.netgrif.application.engine.utils.SingleItemAsList; | ||
| import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Custom deserializer for handling JSON deserialization of objects that extend | ||
| * the {@link SingleItemAsList} class, specifically designed for handling | ||
| * {@link CaseSearchRequest} and ensuring its fields are properly sanitized. | ||
| * <p> | ||
| * This deserializer extends the functionality of {@link SingleItemAsListDeserializer} | ||
| * to additionally process the deserialized objects that represent case search requests. | ||
| * It ensures that the `fullText` field in each case search request is sanitized | ||
| * using {@link ElasticsearchQuerySanitizer}. | ||
| * <p> | ||
| * It also provides a mechanism to dynamically determine the appropriate type | ||
| * using the contextual information during deserialization. | ||
| */ | ||
| public class CaseSearchRequestSingleItemAsListDeserializer extends SingleItemAsListDeserializer { | ||
|
|
||
| protected CaseSearchRequestSingleItemAsListDeserializer() { | ||
| this(null); | ||
| } | ||
|
|
||
| protected CaseSearchRequestSingleItemAsListDeserializer(Class<? extends SingleItemAsList> vc) { | ||
| super(vc); | ||
| } | ||
|
|
||
| @Override | ||
| public JsonDeserializer<?> createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty) { | ||
| return new CaseSearchRequestSingleItemAsListDeserializer((Class<? extends SingleItemAsList>) getItemClass(deserializationContext, beanProperty)); | ||
| } | ||
|
|
||
| /** | ||
| * Deserializes a JSON structure into an object, specifically handling instances that | ||
| * may extend the {@code SingleCaseSearchRequestAsList}. During deserialization, it | ||
| * sanitizes the `fullText` field in each {@code CaseSearchRequest} object for security | ||
| * purposes using {@code ElasticsearchQuerySanitizer}. | ||
| * | ||
| * @param jsonParser the {@code JsonParser} used for reading the JSON input | ||
| * @param deserializationContext the {@code DeserializationContext} providing access | ||
| * to contextual information during deserialization | ||
| * @return the deserialized object, with sanitization applied if it is an instance of | ||
| * {@code SingleCaseSearchRequestAsList} | ||
| * @throws IOException if any I/O error occurs during deserialization | ||
| * @throws IllegalArgumentException if the object could not be properly instantiated or deserialized | ||
| */ | ||
| @Override | ||
| public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, IllegalArgumentException { | ||
| Object result = super.deserialize(jsonParser, deserializationContext); | ||
| if (isWrapperClass(result, SingleCaseSearchRequestAsList.class, CaseSearchRequest.class)) { | ||
| List<CaseSearchRequest> list = ((SingleCaseSearchRequestAsList) result).getList(); | ||
| list.forEach(request -> | ||
| request.fullText = ElasticsearchQuerySanitizer.sanitize(request.fullText)); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| } |
74 changes: 74 additions & 0 deletions
74
...grif/application/engine/workflow/utils/TaskSearchRequestSingleItemAsListDeserializer.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package com.netgrif.application.engine.workflow.utils; | ||
|
|
||
| import com.fasterxml.jackson.core.JsonParser; | ||
| import com.fasterxml.jackson.databind.BeanProperty; | ||
| import com.fasterxml.jackson.databind.DeserializationContext; | ||
| import com.fasterxml.jackson.databind.JsonDeserializer; | ||
| import com.netgrif.application.engine.elastic.service.ElasticsearchQuerySanitizer; | ||
| import com.netgrif.application.engine.elastic.web.requestbodies.ElasticTaskSearchRequest; | ||
| import com.netgrif.application.engine.elastic.web.requestbodies.singleaslist.SingleElasticTaskSearchRequestAsList; | ||
| import com.netgrif.application.engine.utils.SingleItemAsList; | ||
| import com.netgrif.application.engine.utils.SingleItemAsListDeserializer; | ||
| import com.netgrif.application.engine.workflow.web.requestbodies.TaskSearchRequest; | ||
| import com.netgrif.application.engine.workflow.web.requestbodies.singleaslist.SingleTaskSearchRequestAsList; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Custom deserializer for handling cases where single `TaskSearchRequest` items | ||
| * are sent as lists or standalone entities during JSON deserialization. | ||
| * <p> | ||
| * This class extends the `SingleItemAsListDeserializer`, enabling support for | ||
| * deserialization scenarios where JSON may represent either a single item or a list of items. | ||
| * It ensures compatibility with `SingleTaskSearchRequestAsList` by sanitizing the `fullText` field | ||
| * in each `TaskSearchRequest` instance using the `ElasticsearchQuerySanitizer`. | ||
| */ | ||
| public class TaskSearchRequestSingleItemAsListDeserializer extends SingleItemAsListDeserializer { | ||
|
|
||
| protected TaskSearchRequestSingleItemAsListDeserializer() { | ||
| this(null); | ||
| } | ||
|
|
||
| protected TaskSearchRequestSingleItemAsListDeserializer(Class<? extends SingleItemAsList> vc) { | ||
| super(vc); | ||
| } | ||
|
|
||
| @Override | ||
| public JsonDeserializer<?> createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty) { | ||
| return new TaskSearchRequestSingleItemAsListDeserializer((Class<? extends SingleItemAsList>) getItemClass(deserializationContext, beanProperty)); | ||
| } | ||
|
|
||
| /** | ||
| * Deserializes a JSON input into an object while handling cases where a single | ||
| * `TaskSearchRequest` or a list of `TaskSearchRequest` objects is included. If | ||
| * the object is a `SingleTaskSearchRequestAsList`, it processes each `TaskSearchRequest` | ||
| * in the list by sanitizing the `fullText` field using `ElasticsearchQuerySanitizer`. | ||
| * | ||
| * @param jsonParser the JSON parser used to parse the incoming JSON content | ||
| * @param deserializationContext the context for deserialization, providing shared | ||
| * state and configuration | ||
| * @return the deserialized object, with sanitization applied to `TaskSearchRequest.fullText` | ||
| * if applicable | ||
| * @throws IOException if an I/O error occurs during parsing | ||
| * @throws IllegalArgumentException if the deserialization process encounters an error | ||
| */ | ||
| @Override | ||
| public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, IllegalArgumentException { | ||
| Object result = super.deserialize(jsonParser, deserializationContext); | ||
| if (isWrapperClass(result, SingleTaskSearchRequestAsList.class, TaskSearchRequest.class) || | ||
| isWrapperClass(result, SingleElasticTaskSearchRequestAsList.class, ElasticTaskSearchRequest.class)) { | ||
| List<? extends TaskSearchRequest> list = Collections.emptyList(); | ||
| if (result instanceof SingleTaskSearchRequestAsList) { | ||
| list = ((SingleTaskSearchRequestAsList) result).getList(); | ||
| } else if (result instanceof SingleElasticTaskSearchRequestAsList) { | ||
| list = ((SingleElasticTaskSearchRequestAsList) result).getList(); | ||
| } | ||
| list.forEach(request -> | ||
| request.fullText = ElasticsearchQuerySanitizer.sanitize(request.fullText)); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| return result; | ||
| } | ||
|
|
||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.