From f30c2ea8fbce0e41fc7ea2c6eea8f744da24c7a1 Mon Sep 17 00:00:00 2001 From: Smruti Prakash Sahoo Date: Wed, 18 Dec 2024 13:39:11 +0530 Subject: [PATCH] fix(rest): Prevent stored XSS Signed-off-by: Smruti Prakash Sahoo --- pom.xml | 1 + .../authserver/Sw360AuthorizationServer.java | 3 +- .../resourceserver/Sw360ResourceServer.java | 3 +- .../security/ResourceServerConfiguration.java | 3 + rest/rest-common/pom.xml | 6 + .../rest/common/Sw360XSSRequestWrapper.java | 113 ++++++++++++++++++ .../sw360/rest/common/Sw360XssFilter.java | 35 ++++++ 7 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 rest/rest-common/src/main/java/org/eclipse/sw360/rest/common/Sw360XSSRequestWrapper.java create mode 100644 rest/rest-common/src/main/java/org/eclipse/sw360/rest/common/Sw360XssFilter.java diff --git a/pom.xml b/pom.xml index b6c908c2a5..19c93c6c4a 100644 --- a/pom.xml +++ b/pom.xml @@ -162,6 +162,7 @@ 2.3.1 26.0.7 6.3.3 + 1.3.1 diff --git a/rest/authorization-server/src/main/java/org/eclipse/sw360/rest/authserver/Sw360AuthorizationServer.java b/rest/authorization-server/src/main/java/org/eclipse/sw360/rest/authserver/Sw360AuthorizationServer.java index d98b186fb6..8ede315a96 100644 --- a/rest/authorization-server/src/main/java/org/eclipse/sw360/rest/authserver/Sw360AuthorizationServer.java +++ b/rest/authorization-server/src/main/java/org/eclipse/sw360/rest/authserver/Sw360AuthorizationServer.java @@ -16,13 +16,14 @@ import org.eclipse.sw360.datahandler.thrift.users.UserGroup; import org.eclipse.sw360.rest.common.PropertyUtils; import org.eclipse.sw360.rest.common.Sw360CORSFilter; +import org.eclipse.sw360.rest.common.Sw360XssFilter; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; import org.springframework.context.annotation.Import; @SpringBootApplication -@Import(Sw360CORSFilter.class) +@Import({Sw360CORSFilter.class, Sw360XssFilter.class}) public class Sw360AuthorizationServer extends SpringBootServletInitializer { private static final String SW360_PROPERTIES_FILE_PATH = "/sw360.properties"; diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/Sw360ResourceServer.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/Sw360ResourceServer.java index 5dfa583f0e..98013f3eb0 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/Sw360ResourceServer.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/Sw360ResourceServer.java @@ -27,6 +27,7 @@ import org.eclipse.sw360.datahandler.thrift.users.UserGroup; import org.eclipse.sw360.rest.common.PropertyUtils; import org.eclipse.sw360.rest.common.Sw360CORSFilter; +import org.eclipse.sw360.rest.common.Sw360XssFilter; import org.eclipse.sw360.rest.resourceserver.core.OpenAPIPaginationHelper; import org.eclipse.sw360.rest.resourceserver.core.RestControllerHelper; import org.eclipse.sw360.rest.resourceserver.security.apiToken.ApiTokenAuthenticationFilter; @@ -50,7 +51,7 @@ import java.util.*; @SpringBootApplication -@Import(Sw360CORSFilter.class) +@Import({Sw360CORSFilter.class, Sw360XssFilter.class}) public class Sw360ResourceServer extends SpringBootServletInitializer { public static final String REST_BASE_PATH = "/api"; diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/security/ResourceServerConfiguration.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/security/ResourceServerConfiguration.java index b9ac930ad0..cbbb98830f 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/security/ResourceServerConfiguration.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/security/ResourceServerConfiguration.java @@ -34,6 +34,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter; @Profile("!SECURITY_MOCK") @Configuration @@ -74,6 +75,8 @@ public SecurityFilterChain securityFilterChainRS1(HttpSecurity http) throws Exce .jwkSetUri(issuerUri))) .httpBasic(Customizer.withDefaults()) .exceptionHandling(x -> x.authenticationEntryPoint(saep)) + .headers(headers -> headers.xssProtection(xXssConfig -> xXssConfig.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)) + .contentSecurityPolicy(cps -> cps.policyDirectives("script-src 'self'"))) .csrf(csrf -> csrf.disable()).build(); } diff --git a/rest/rest-common/pom.xml b/rest/rest-common/pom.xml index 31cb98a649..56bffc6941 100644 --- a/rest/rest-common/pom.xml +++ b/rest/rest-common/pom.xml @@ -42,6 +42,12 @@ jakarta.servlet-api provided + + org.owasp.encoder + encoder + ${org.owasp.encoder.version} + + rest-common diff --git a/rest/rest-common/src/main/java/org/eclipse/sw360/rest/common/Sw360XSSRequestWrapper.java b/rest/rest-common/src/main/java/org/eclipse/sw360/rest/common/Sw360XSSRequestWrapper.java new file mode 100644 index 0000000000..60e445a69e --- /dev/null +++ b/rest/rest-common/src/main/java/org/eclipse/sw360/rest/common/Sw360XSSRequestWrapper.java @@ -0,0 +1,113 @@ +/* +SPDX-FileCopyrightText: © 2024 Siemens AG +SPDX-License-Identifier: EPL-2.0 +*/ +package org.eclipse.sw360.rest.common; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +/** + * This class is used to sanitize the input from the user to prevent XSS attacks. + * + * @author smruti.sahoo@siemens.com + */ +public class Sw360XSSRequestWrapper extends HttpServletRequestWrapper { + + public Sw360XSSRequestWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + ServletInputStream originalInputStream = super.getInputStream(); + String requestBody = new String(originalInputStream.readAllBytes()); + + JsonNode requestBodyJSON = sanitizeInput(new ObjectMapper().readTree(requestBody)); + String sanitizedBody = requestBodyJSON.toString(); + return new ServletInputStream() { + private final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( + sanitizedBody.getBytes() + ); + + @Override + public int read() throws IOException { + return byteArrayInputStream.read(); + } + + @Override + public boolean isFinished() { + return byteArrayInputStream.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + }; + } + + @Override + public String[] getParameterValues(String parameter) { + String[] values = super.getParameterValues(parameter); + if (values == null) { + return null; + } + int count = values.length; + String[] encodedValues = new String[count]; + for (int i = 0; i < count; i++) { + encodedValues[i] = stripXSS(values[i]); + } + return encodedValues; + } + + @Override + public String getParameter(String parameter) { + String value = super.getParameter(parameter); + return stripXSS(value); + } + + @Override + public String getHeader(String name) { + String value = super.getHeader(name); + return stripXSS(value); + } + + private String stripXSS(String value) { + return org.owasp.encoder.Encode.forHtml(value); + + } + + private JsonNode sanitizeInput(JsonNode input) { + if (input.isTextual()) { + return JsonNodeFactory.instance.textNode(stripXSS(input.asText())); + } else if (input.isArray()) { + ArrayNode arrayNode = JsonNodeFactory.instance.arrayNode(); + for (JsonNode element : input) { + arrayNode.add(sanitizeInput(element)); + } + return arrayNode; + } else if (input.isObject()) { + ObjectNode objectNode = JsonNodeFactory.instance.objectNode(); + input.fields().forEachRemaining(entry -> objectNode.set(entry.getKey(), sanitizeInput(entry.getValue()))); + return objectNode; + } else { + return input; + } + } + +} diff --git a/rest/rest-common/src/main/java/org/eclipse/sw360/rest/common/Sw360XssFilter.java b/rest/rest-common/src/main/java/org/eclipse/sw360/rest/common/Sw360XssFilter.java new file mode 100644 index 0000000000..312dd866f3 --- /dev/null +++ b/rest/rest-common/src/main/java/org/eclipse/sw360/rest/common/Sw360XssFilter.java @@ -0,0 +1,35 @@ +/* +SPDX-FileCopyrightText: © 2024 Siemens AG +SPDX-License-Identifier: EPL-2.0 +*/ +package org.eclipse.sw360.rest.common; + + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import java.io.IOException; + +/** + * This filter is used to sanitize the input from the user to prevent XSS attacks. + * @author smruti.sahoo@siemens.com + */ +@Configuration +@Order(Ordered.HIGHEST_PRECEDENCE) +public class Sw360XssFilter implements Filter { + + /** + * @param servletRequest + * @param servletResponse + * @param filterChain + * @throws IOException + * @throws ServletException + */ + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + filterChain.doFilter(new Sw360XSSRequestWrapper((HttpServletRequest) servletRequest), servletResponse); + } +}