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 8 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
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,18 @@
<version>0.0.39</version>
</dependency>

<dependency>
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
<artifactId>owasp-java-html-sanitizer</artifactId>
<version>20220608.1</version>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.10.0</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
137 changes: 137 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,137 @@
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.owasp.html.HtmlPolicyBuilder;
import org.owasp.html.PolicyFactory;

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;

private final PolicyFactory policy = new HtmlPolicyBuilder().toFactory();

@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.


/**
* Method to sanitize the html
* @param input - Input string
* @return String - Sanitized string
*/
private String sanitizeHtml(String input) {
String sanitizedInput = policy.sanitize(input);
return sanitizedInput;
}

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

}
9 changes: 9 additions & 0 deletions src/main/java/com/salessparrow/api/lib/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,13 @@ public String getDateFormatFromDatetime(Date date) {
return null;
}

/**
* Unescape special characters in a string. Currently, only "&" is unescaped.
* @param input
* @return Unescaped string
*/
public String unEscapeSpecialCharacters(String input) {
return input.replace("&amp;", "&");
}
kreloaded marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,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.unEscapeSpecialCharacters(noteText);

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

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,17 @@ private CreateTaskFormatterDto parseResponse(String createTaskResponse) {
*/
private String getTaskSubjectFromDescription(CreateAccountTaskDto task) {
logger.info("getting task subject from description");
if (task.getDescription().length() < 60) {
return task.getDescription();

String taskDescription = task.getDescription();

Util util = new Util();
taskDescription = util.unEscapeSpecialCharacters(taskDescription);

if (taskDescription.length() < 60) {
return taskDescription;
}

return task.getDescription().substring(0, 60);
return taskDescription.substring(0, 60);
}
kreloaded marked this conversation as resolved.
Show resolved Hide resolved

}
Loading
Loading