Skip to content
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

Added sanitization filter for request sanitization #78

Merged
merged 22 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
30 changes: 15 additions & 15 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
version: '3'
version: "3"
services:
api:
build:
context: .
dockerfile: Dockerfile.local
ports:
- '8080:8080'
- "8080:8080"
networks:
- localdev
volumes:
- './:/app'
- '~/.m2:/root/.m2'
- "./:/app"
- "~/.m2:/root/.m2"
environment:
- ENVIRONMENT=development
- AWS_ACCESS_KEY_ID=local
Expand All @@ -27,12 +27,12 @@ services:
context: .
dockerfile: Dockerfile.local
ports:
- '8080:8080'
- "8080:8080"
networks:
- localdev
volumes:
- './:/app'
- '~/.m2:/root/.m2'
- "./:/app"
- "~/.m2:/root/.m2"
environment:
- ENVIRONMENT=local-test
- AWS_ACCESS_KEY_ID=local
Expand All @@ -47,14 +47,14 @@ services:
memcached:
image: memcached
ports:
- '11211:11211'
- "11211:11211"
networks:
- localdev

localkms:
image: nsmithuk/local-kms
ports:
- '4599:8080'
- "4599:8080"
volumes:
- ./init:/init
networks:
Expand All @@ -66,18 +66,18 @@ services:
- KMS_ACCOUNT_ID='111122223333'
- KMS_REGION='us-east-1'
- KMS_SEED_PATH=init/seed.yaml

dynamodb:
image: amazon/dynamodb-local:latest
ports:
- '8000:8000'
- "8000:8000"
networks:
- localdev
volumes:
- "./docker/dynamodb:/home/dynamodblocal/data"
- "./docker/dynamodb:/home/dynamodblocal/data"
working_dir: /home/dynamodblocal
command: '-jar DynamoDBLocal.jar -sharedDb -dbPath ./data'
command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"

networks:
localdev:
driver: bridge
driver: bridge
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@
<version>0.0.39</version>
</dependency>

<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.16.1</version>
</dependency>

</dependencies>

<build>
Expand Down
2 changes: 1 addition & 1 deletion repo-docs/LOCAL_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ $ touch test.secrets.json
"ERROR_MAIL_FROM": "",
"ERROR_MAIL_TO": "",
"COOKIE_DOMAIN": "",
"OPENAI_API_KEY: ""
"OPENAI_API_KEY": ""
}
```

Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/salessparrow/api/config/FilterConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.context.annotation.Configuration;

import com.salessparrow.api.filter.SameSiteCookieFilter;
import com.salessparrow.api.filter.SanitizationFilter;

@Configuration
public class FilterConfig {
Expand All @@ -18,6 +19,20 @@ public FilterRegistrationBean<SameSiteCookieFilter> sameSiteCookieFilter() {
FilterRegistrationBean<SameSiteCookieFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new SameSiteCookieFilter());
registrationBean.addUrlPatterns("/*"); // or specific URL patterns
registrationBean.setOrder(1);
return registrationBean;
}

/**
* Register SanitizationFilter
* @return FilterRegistrationBean<SanitizationFilter>
*/
@Bean
public FilterRegistrationBean<SanitizationFilter> sanitizationFilter() {
FilterRegistrationBean<SanitizationFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new SanitizationFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(0);
return registrationBean;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,16 @@ public InterceptorConfig(UserAuthInterceptor userAuthInterceptor, JsonOnlyInterc

@Override
public void addInterceptors(InterceptorRegistry registry) {
/* Add logger interceptor to all the routes */
registry.addInterceptor(loggerInterceptor).addPathPatterns("/**");

/* Add json only interceptor to all the routes */
registry.addInterceptor(jsonOnlyInterceptor).addPathPatterns("/**");

/* Add v1 interceptor only to all the routes */
registry.addInterceptor(V1Interceptor).addPathPatterns("/api/v1/**");

/* Add user auth interceptor to all the routes except the ones below */
registry.addInterceptor(userAuthInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/api/v1/auth/salesforce/**")
Expand Down
159 changes: 159 additions & 0 deletions src/main/java/com/salessparrow/api/filter/SanitizationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package com.salessparrow.api.filter;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;
import org.jsoup.nodes.Entities.EscapeMode;

import com.salessparrow.api.lib.wrappers.SanitizedRequestWrapper;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.Enumeration;

/**
* Class to sanitize the request
*/
public class SanitizationFilter implements Filter {

private SanitizedRequestWrapper sanitizedRequest;

@Override
public void init(FilterConfig filterConfig) {
}

/**
* Method to sanitize the request
* @param servletRequest - Servlet request object
* @param servletResponse - Servlet response object
* @param chain - Filter chain - Filter chain
* @throws IOException - IOException
* @throws ServletException - ServletException
* @return void
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
if (!(servletRequest instanceof HttpServletRequest)) {
throw new ServletException("Can only process HttpServletRequest");
}
Comment on lines +42 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filter should be able to handle all types of ServletRequest. Instead of throwing an exception when the request is not an instance of HttpServletRequest, consider skipping the sanitization process and just pass the request to the next filter in the chain.

- if (!(servletRequest instanceof HttpServletRequest)) {
-     throw new ServletException("Can only process HttpServletRequest");
- }
+ if (!(servletRequest instanceof HttpServletRequest)) {
+     chain.doFilter(servletRequest, servletResponse);
+     return;
+ }

HttpServletRequest request = (HttpServletRequest) servletRequest;
sanitizeRequestBody(request);
sanitizeRequestParams(request);
sanitizeRequestHeaders(request);

chain.doFilter(sanitizedRequest, servletResponse);
}
kreloaded marked this conversation as resolved.
Show resolved Hide resolved

/**
* Method to sanitize the request body
* @param request - Servlet request object
* @return void
*/
private void sanitizeRequestBody(HttpServletRequest request) {
String originalBody = getRequestBody(request);
String sanitizedBody = sanitizeHtml(originalBody);
this.sanitizedRequest = new SanitizedRequestWrapper(request, sanitizedBody);
}
kreloaded marked this conversation as resolved.
Show resolved Hide resolved
kreloaded marked this conversation as resolved.
Show resolved Hide resolved

/**
* Method to sanitize the request params
* @return void
*/
private void sanitizeRequestParams(HttpServletRequest request) {
request.getParameterMap().forEach((key, values) -> {
for (int index = 0; index < values.length; index++) {
String sanitizedValue = sanitizeHtml(values[index]);
this.sanitizedRequest.setParameter(key, sanitizedValue);
}
});
kreloaded marked this conversation as resolved.
Show resolved Hide resolved
kreloaded marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Method to sanitize the request headers
* @return void
*/
private void sanitizeRequestHeaders(HttpServletRequest request) {
Enumeration<String> headerNames = request.getHeaderNames();

while (headerNames != null && headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
Enumeration<String> headerValues = request.getHeaders(headerName);

while (headerValues != null && headerValues.hasMoreElements()) {
String headerValue = headerValues.nextElement();
String sanitizedValue = sanitizeHtml(headerValue);
this.sanitizedRequest.setHeader(headerName, sanitizedValue);
}
}
kreloaded marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Method to get the request body
* @param request - Servlet request object
* @return String - Request body
*/
private String getRequestBody(HttpServletRequest request) {
try {
BufferedReader reader = request.getReader();
String line;
// TODO: Consider using a more efficient way to read the request body, such as
// streaming
StringBuilder requestBody = new StringBuilder();
while ((line = reader.readLine()) != null) {
requestBody.append(line);
}
return requestBody.toString();
}
catch (IOException e) {
throw new RuntimeException("Error reading request body: ", e.getCause());
}
Comment on lines +104 to +117
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BufferedReader is not closed after use which can lead to resource leaks. Always close resources in a finally block or use try-with-resources statement.

- BufferedReader reader = request.getReader();
- String line;
- StringBuilder requestBody = new StringBuilder();
- while ((line = reader.readLine()) != null) {
-     requestBody.append(line);
- }
- return requestBody.toString();
+ try (BufferedReader reader = request.getReader()) {
+     String line;
+     StringBuilder requestBody = new StringBuilder();
+     while ((line = reader.readLine()) != null) {
+         requestBody.append(line);
+     }
+     return requestBody.toString();
+ } catch (IOException e) {
+     throw new RuntimeException("Error reading request body: ", e.getCause());
+ }

}
Comment on lines +104 to +118
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method getRequestBody reads the entire request body into memory which can lead to OutOfMemoryError for large requests. Consider using a more efficient way to read the request body, such as streaming or limiting the size of the request body that can be read.


/**
* Sanitizes a given HTML string by removing all HTML tags and attributes, leaving
* only the text content. Also handles basic HTML entity escaping using the 'base'
* escape mode.
*
* This function uses the Jsoup library's 'clean' method with a 'none' safelist, which
* means no HTML tags are allowed and will be removed.
*
* The function also disables pretty-printing to ensure the resulting HTML is not
* reformatted, and sets the escape mode to 'base' to handle basic HTML entities like
* &amp; and &lt;.
*
* If the input is null or empty, the function returns an empty string.
* @param input The original HTML string that needs to be sanitized.
* @return A sanitized version of the input string, safe for display as text.
*/
private String sanitizeHtml(String input) {
if (input == null || input.isEmpty()) {
return "";
}

Safelist safelist = Safelist.none();

Document.OutputSettings outputSettings = new Document.OutputSettings();
outputSettings.prettyPrint(false);
outputSettings.escapeMode(EscapeMode.base);

String sanitizedInput = Jsoup.clean(input, "", safelist, outputSettings);

return sanitizedInput;
}
Comment on lines +136 to +150
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method sanitizeHtml creates a new Safelist and Document.OutputSettings object for each call. This can be inefficient for large number of requests. Consider making these objects as class level constants.


/**
* Method to destroy the filter
*/
@Override
public void destroy() {
}

}
19 changes: 18 additions & 1 deletion src/main/java/com/salessparrow/api/lib/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public static String generateHeaderLogString(HttpServletRequest request) {
}

/**
* <<<<<<< HEAD Encode plain text to base64
* Encode plain text to base64
* @param plainText - String to be encoded
* @return String - Encoded string
*/
Expand Down Expand Up @@ -160,4 +160,21 @@ public String getDateFormatFromDatetime(Date date) {
return null;
}

/**
* Unescape special characters in a string intended for plaintext contexts only.
*
* This function is intended for use only with strings that will be rendered as
* plaintext. It is NOT suitable for HTML strings, XML strings, or any other context
* where special characters may have syntactic meaning.
*
* Current Implementation: - The ampersand ("&amp;") is unescaped to "&"
*
* Future versions may include additional un-escaping rules as needed.
* @param input The original string containing escaped special characters.
* @return A new string where certain special characters have been unescaped.
*/
public String unEscapeSpecialCharactersForPlainText(String input) {
return input.replace("&amp;", "&");
}
kreloaded marked this conversation as resolved.
Show resolved Hide resolved
kreloaded marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public class CreateSalesforceNote implements CreateNoteInterface {
public CreateNoteFormatterDto createNote(SalesforceUser user, String accountId, NoteDto note) {
String salesforceUserId = user.getExternalUserId();

logger.info("createNote note text: ", note.getText());
String noteTitle = getNoteTitleFromContent(note);
String encodedNoteContent = base64Helper.base64Encode(note.getText());

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logger.info statement at line 57 is not correctly formatted. It should include a placeholder {} for the variable note.getText(). This will prevent unnecessary string concatenation when logging is disabled and ensure that the note text is properly included in the log message.

- logger.info("createNote note text: ", note.getText());
+ logger.info("createNote note text: {}", note.getText());

Expand Down Expand Up @@ -127,11 +128,16 @@ private CreateNoteFormatterDto parseResponse(String createNoteResponse) {
* @return String
*/
private String getNoteTitleFromContent(NoteDto note) {
if (note.getText().length() < 50) {
return note.getText();
String noteText = note.getText();

Util util = new Util();
noteText = util.unEscapeSpecialCharactersForPlainText(noteText);

if (noteText.length() < 50) {
return noteText;
}

return note.getText().substring(0, 50);
return noteText.substring(0, 50);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a new instance of Util class every time the method getNoteTitleFromContent is called can be inefficient. Consider making unEscapeSpecialCharactersForPlainText a static method, so you don't need to create an instance of Util each time.

-		Util util = new Util();
-		noteText = util.unEscapeSpecialCharactersForPlainText(noteText);
+		noteText = Util.unEscapeSpecialCharactersForPlainText(noteText);


}
Loading
Loading