From 43dca3c4ad33c058c02a0aadb4c8a2a6911f8c9a Mon Sep 17 00:00:00 2001 From: Rashidi Zin Date: Mon, 19 May 2025 23:47:54 +0800 Subject: [PATCH 1/4] Add XSS protection example --- web-thymeleaf-xss/.gitattributes | 3 ++ web-thymeleaf-xss/.gitignore | 37 +++++++++++++++++++ web-thymeleaf-xss/build.gradle.kts | 32 ++++++++++++++++ web-thymeleaf-xss/settings.gradle.kts | 1 + .../web/xss/WebThymeleafXssApplication.java | 13 +++++++ .../rashidi/web/xss/greet/GreetResource.java | 21 +++++++++++ .../xss/security/SecurityConfiguration.java | 28 ++++++++++++++ .../src/main/resources/application.properties | 1 + .../src/main/resources/templates/greet.html | 10 +++++ .../xss/WebThymeleafXssApplicationTests.java | 13 +++++++ .../web/xss/greet/GreetResourceTests.java | 35 ++++++++++++++++++ 11 files changed, 194 insertions(+) create mode 100644 web-thymeleaf-xss/.gitattributes create mode 100644 web-thymeleaf-xss/.gitignore create mode 100644 web-thymeleaf-xss/build.gradle.kts create mode 100644 web-thymeleaf-xss/settings.gradle.kts create mode 100644 web-thymeleaf-xss/src/main/java/zin/rashidi/web/xss/WebThymeleafXssApplication.java create mode 100644 web-thymeleaf-xss/src/main/java/zin/rashidi/web/xss/greet/GreetResource.java create mode 100644 web-thymeleaf-xss/src/main/java/zin/rashidi/web/xss/security/SecurityConfiguration.java create mode 100644 web-thymeleaf-xss/src/main/resources/application.properties create mode 100644 web-thymeleaf-xss/src/main/resources/templates/greet.html create mode 100644 web-thymeleaf-xss/src/test/java/zin/rashidi/web/xss/WebThymeleafXssApplicationTests.java create mode 100644 web-thymeleaf-xss/src/test/java/zin/rashidi/web/xss/greet/GreetResourceTests.java diff --git a/web-thymeleaf-xss/.gitattributes b/web-thymeleaf-xss/.gitattributes new file mode 100644 index 00000000..8af972cd --- /dev/null +++ b/web-thymeleaf-xss/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/web-thymeleaf-xss/.gitignore b/web-thymeleaf-xss/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/web-thymeleaf-xss/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/web-thymeleaf-xss/build.gradle.kts b/web-thymeleaf-xss/build.gradle.kts new file mode 100644 index 00000000..001b3407 --- /dev/null +++ b/web-thymeleaf-xss/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + java + id("org.springframework.boot") version "3.4.5" + id("io.spring.dependency-management") version "1.1.7" +} + +group = "zin.rashidi" +version = "0.0.1-SNAPSHOT" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-thymeleaf") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.security:spring-security-test") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/web-thymeleaf-xss/settings.gradle.kts b/web-thymeleaf-xss/settings.gradle.kts new file mode 100644 index 00000000..ad0e708c --- /dev/null +++ b/web-thymeleaf-xss/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "web-thymeleaf-xss" diff --git a/web-thymeleaf-xss/src/main/java/zin/rashidi/web/xss/WebThymeleafXssApplication.java b/web-thymeleaf-xss/src/main/java/zin/rashidi/web/xss/WebThymeleafXssApplication.java new file mode 100644 index 00000000..46cde7d4 --- /dev/null +++ b/web-thymeleaf-xss/src/main/java/zin/rashidi/web/xss/WebThymeleafXssApplication.java @@ -0,0 +1,13 @@ +package zin.rashidi.web.xss; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class WebThymeleafXssApplication { + + public static void main(String[] args) { + SpringApplication.run(WebThymeleafXssApplication.class, args); + } + +} diff --git a/web-thymeleaf-xss/src/main/java/zin/rashidi/web/xss/greet/GreetResource.java b/web-thymeleaf-xss/src/main/java/zin/rashidi/web/xss/greet/GreetResource.java new file mode 100644 index 00000000..75bc9b39 --- /dev/null +++ b/web-thymeleaf-xss/src/main/java/zin/rashidi/web/xss/greet/GreetResource.java @@ -0,0 +1,21 @@ +package zin.rashidi.web.xss.greet; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * @author Rashidi Zin + */ +@Controller +class GreetResource { + + @GetMapping("/greet") + public String greet(@RequestParam String name, Model model) { + model.addAttribute("name", name); + + return "greet"; + } + +} diff --git a/web-thymeleaf-xss/src/main/java/zin/rashidi/web/xss/security/SecurityConfiguration.java b/web-thymeleaf-xss/src/main/java/zin/rashidi/web/xss/security/SecurityConfiguration.java new file mode 100644 index 00000000..06932bbb --- /dev/null +++ b/web-thymeleaf-xss/src/main/java/zin/rashidi/web/xss/security/SecurityConfiguration.java @@ -0,0 +1,28 @@ +package zin.rashidi.web.xss.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; + +import static org.springframework.security.web.header.writers.XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK; + +/** + * @author Rashidi Zin + */ +@Configuration +@EnableWebSecurity +class SecurityConfiguration { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + .headers(headers -> headers + .contentSecurityPolicy(policy -> policy.policyDirectives("default-src 'self'")) + .xssProtection(xss -> xss.headerValue(ENABLED_MODE_BLOCK)) + ) + .build(); + } + +} diff --git a/web-thymeleaf-xss/src/main/resources/application.properties b/web-thymeleaf-xss/src/main/resources/application.properties new file mode 100644 index 00000000..84ddb44d --- /dev/null +++ b/web-thymeleaf-xss/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=web-thymeleaf-xss diff --git a/web-thymeleaf-xss/src/main/resources/templates/greet.html b/web-thymeleaf-xss/src/main/resources/templates/greet.html new file mode 100644 index 00000000..6010cdab --- /dev/null +++ b/web-thymeleaf-xss/src/main/resources/templates/greet.html @@ -0,0 +1,10 @@ + + + + + Greeting + + +

+ + \ No newline at end of file diff --git a/web-thymeleaf-xss/src/test/java/zin/rashidi/web/xss/WebThymeleafXssApplicationTests.java b/web-thymeleaf-xss/src/test/java/zin/rashidi/web/xss/WebThymeleafXssApplicationTests.java new file mode 100644 index 00000000..633fac4e --- /dev/null +++ b/web-thymeleaf-xss/src/test/java/zin/rashidi/web/xss/WebThymeleafXssApplicationTests.java @@ -0,0 +1,13 @@ +package zin.rashidi.web.xss; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class WebThymeleafXssApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/web-thymeleaf-xss/src/test/java/zin/rashidi/web/xss/greet/GreetResourceTests.java b/web-thymeleaf-xss/src/test/java/zin/rashidi/web/xss/greet/GreetResourceTests.java new file mode 100644 index 00000000..a5cf1c73 --- /dev/null +++ b/web-thymeleaf-xss/src/test/java/zin/rashidi/web/xss/greet/GreetResourceTests.java @@ -0,0 +1,35 @@ +package zin.rashidi.web.xss.greet; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.assertj.MockMvcTester; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Rashidi Zin + */ +@SpringBootTest(webEnvironment = RANDOM_PORT) +@AutoConfigureMockMvc +class GreetResourceTests { + + @Autowired + private MockMvcTester mvc; + + @Test + @DisplayName("Given XSS protection is enabled Then response header should contain information about X-XSS-Protection and Content-Security-Policy") + void headers() { + mvc.get() + .uri("/greet?name={name}", "rashidi") + .assertThat() + .matches(status().isOk()) + .matches(header().string("Content-Security-Policy", "default-src 'self'")) + .matches(header().string("X-XSS-Protection", "1; mode=block")); + } + +} \ No newline at end of file From adeffe7316db5fc153e56c32e0a69218a8e3de41 Mon Sep 17 00:00:00 2001 From: Rashidi Zin Date: Sun, 25 May 2025 12:31:10 +0800 Subject: [PATCH 2/4] Bump spring boot to 3.5.0 --- web-thymeleaf-xss/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-thymeleaf-xss/build.gradle.kts b/web-thymeleaf-xss/build.gradle.kts index 001b3407..4ebac0e8 100644 --- a/web-thymeleaf-xss/build.gradle.kts +++ b/web-thymeleaf-xss/build.gradle.kts @@ -1,6 +1,6 @@ plugins { java - id("org.springframework.boot") version "3.4.5" + id("org.springframework.boot") version "3.5.0" id("io.spring.dependency-management") version "1.1.7" } From e7d2fe7b3669cf1f33f05540ab0371e1cbe71394 Mon Sep 17 00:00:00 2001 From: Rashidi Zin Date: Sun, 25 May 2025 12:36:29 +0800 Subject: [PATCH 3/4] Cleanup HTML --- web-thymeleaf-xss/src/main/resources/templates/greet.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-thymeleaf-xss/src/main/resources/templates/greet.html b/web-thymeleaf-xss/src/main/resources/templates/greet.html index 6010cdab..f7d2affd 100644 --- a/web-thymeleaf-xss/src/main/resources/templates/greet.html +++ b/web-thymeleaf-xss/src/main/resources/templates/greet.html @@ -5,6 +5,6 @@ Greeting -

+

\ No newline at end of file From ea0882046d14fbd4ff070152373b2f10c9078f57 Mon Sep 17 00:00:00 2001 From: Rashidi Zin Date: Sun, 25 May 2025 15:11:52 +0800 Subject: [PATCH 4/4] Add readme --- README.adoc | 1 + docs/modules/ROOT/nav.adoc | 1 + .../modules/ROOT/pages/web-thymeleaf-xss.adoc | 123 +++++++++++++++++ modulith/docs/components.puml | 2 +- modulith/docs/module-subscription.puml | 2 +- web-thymeleaf-xss/README.adoc | 124 ++++++++++++++++++ .../src/main/resources/templates/greet.html | 2 +- .../web/xss/greet/GreetResourceTests.java | 9 +- 8 files changed, 256 insertions(+), 8 deletions(-) create mode 100644 docs/modules/ROOT/pages/web-thymeleaf-xss.adoc create mode 100644 web-thymeleaf-xss/README.adoc diff --git a/README.adoc b/README.adoc index 3cc3b650..76af5893 100644 --- a/README.adoc +++ b/README.adoc @@ -63,5 +63,6 @@ All tutorials are documented in AsciiDoc format and published as an https://anto |link:test-rest-assured[Spring Test: Integration with RestAssured] | Implement Behaviour Driven Development with https://rest-assured.io/[RestAssured] |link:test-slice-tests-rest[Spring Test: Implementing Slice Tests for REST application] | Dive into available options to implement tests with Spring Boot's test components |link:web-rest-client[Spring Web: REST Clients for calling Synchronous API] | Implement REST client to perform synchronous API calls +|link:web-thymeleaf-xss[Spring Web: Preventing XSS with Thymeleaf] |Prevent Cross-Site Scripting (XSS) attacks in Spring Boot applications using Spring Security and Thymeleaf |link:modulith[Spring Modulith: Building Modular Monolithic Applications] | Structure Spring Boot applications into well-defined modules with clear boundaries |=== diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index a8cd915c..86d3a361 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -30,3 +30,4 @@ ** xref:test-slice-tests-rest.adoc[Implementing Slice Tests for REST application] * Spring Web ** xref:web-rest-client.adoc[REST Clients for calling Synchronous API] +** xref:web-thymeleaf-xss.adoc[] diff --git a/docs/modules/ROOT/pages/web-thymeleaf-xss.adoc b/docs/modules/ROOT/pages/web-thymeleaf-xss.adoc new file mode 100644 index 00000000..bb387e27 --- /dev/null +++ b/docs/modules/ROOT/pages/web-thymeleaf-xss.adoc @@ -0,0 +1,123 @@ += Preventing XSS with Spring Security and Thymeleaf +:source-highlighter: highlight.js +Rashidi Zin +1.0, May 25, 2023 +:icons: font +:source-highlighter: highlight.js +:url-quickref: https://github.com/rashidi/spring-boot-tutorials/tree/master/web-thymeleaf-xss + +Prevent Cross-Site Scripting (XSS) attacks in Spring Boot applications using Spring Security and Thymeleaf. + +== Background + +https://owasp.org/www-community/attacks/xss/[Cross-Site Scripting (XSS)] is a security vulnerability that allows attackers to inject client-side scripts into web pages viewed by other users. This can lead to various attacks, including stealing session cookies, redirecting users to malicious websites, or performing actions on behalf of the user. + +In this tutorial, we will demonstrate how to prevent XSS attacks in a Spring Boot application using Spring Security and Thymeleaf. We will focus on two main approaches: + +1. Using Spring Security to add security headers +2. Leveraging Thymeleaf's automatic HTML escaping + +== Security Configuration + +The first line of defense against XSS attacks is to configure Spring Security to add appropriate security headers. In our example, we use the following configuration: + +[source,java] +---- +@Configuration +@EnableWebSecurity +class SecurityConfiguration { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + .headers(headers -> headers + .contentSecurityPolicy(policy -> policy.policyDirectives("default-src 'self'")) + .xssProtection(xss -> xss.headerValue(ENABLED_MODE_BLOCK)) + ) + .build(); + } +} +---- + +This configuration adds two important security headers: + +1. **Content-Security-Policy**: Restricts the sources from which content can be loaded. In this case, `default-src 'self'` means that the browser should only load resources from the same origin. + +2. **X-XSS-Protection**: Enables the browser's built-in XSS filter. The value `1; mode=block` (represented by `ENABLED_MODE_BLOCK`) tells the browser to block the response if a XSS attack is detected. + +== Thymeleaf Template + +Thymeleaf provides built-in protection against XSS by automatically escaping HTML content. Let's look at our simple greeting template: + +[source,html] +---- + + + + + Greeting + + +

+ + +---- + +The key part here is the use of `th:utext` attribute. Unlike `th:text` which automatically escapes HTML content, `th:utext` (unescaped text) renders HTML as-is. This means that if a user inputs ``, it will be executed as code rather than displayed as text. This approach is used when you need to render HTML content, but it requires careful handling of user input to prevent XSS attacks. + +== Controller Implementation + +Our controller simply takes a name parameter and passes it to the Thymeleaf template: + +[source,java] +---- +@Controller +class GreetResource { + + @GetMapping("/greet") + public String greet(@RequestParam String name, Model model) { + model.addAttribute("name", name); + + return "greet"; + } +} +---- + +== Verifying XSS Protection + +We can verify that our XSS protection is working by checking that the appropriate security headers are present in the response: + +[source,java] +---- +@WebMvcTest(controllers = GreetResource.class, includeFilters = @Filter(classes = EnableWebSecurity.class)) +class GreetResourceTests { + + @Autowired + private MockMvcTester mvc; + + @Test + @DisplayName("Given XSS protection is enabled Then response header should contain information about X-XSS-Protection and Content-Security-Policy") + void headers() { + mvc.get() + .uri("/greet?name={name}", "rashidi") + .assertThat() + .matches(status().isOk()) + .matches(header().string("Content-Security-Policy", "default-src 'self'")) + .matches(header().string("X-XSS-Protection", "1; mode=block")); + } +} +---- + +== Common XSS Vulnerabilities to Avoid + +1. **Understanding the risks of `th:utext`**: As demonstrated in our example, the `th:utext` attribute in Thymeleaf renders unescaped HTML, which can lead to XSS vulnerabilities if not handled properly. While our example uses `th:utext` for demonstration purposes, in production applications you should use `th:text` instead unless you are absolutely sure the content is safe and HTML rendering is required. + +2. **Disabling Content Security Policy**: The Content Security Policy is a powerful defense against XSS. Avoid disabling it or setting overly permissive policies. + +3. **Trusting user input**: Always validate and sanitize user input before processing it, even if you're using automatic escaping. + +== Conclusion + +In this tutorial, we've seen how to implement XSS protection in a Spring Boot application using Spring Security and Thymeleaf. We've demonstrated how to add appropriate security headers through Spring Security configuration. We've also shown how Thymeleaf handles HTML content with `th:utext`, while highlighting the potential security implications and when to use the safer `th:text` alternative instead. + +Remember that security is a multi-layered approach, and XSS protection is just one aspect of a comprehensive security strategy. Always keep your dependencies up to date and follow security best practices to ensure your application remains secure. diff --git a/modulith/docs/components.puml b/modulith/docs/components.puml index 001e3281..98a6ec7b 100644 --- a/modulith/docs/components.puml +++ b/modulith/docs/components.puml @@ -14,8 +14,8 @@ Container_Boundary("ModulithApplication.ModulithApplication_boundary", "Modulith Component(ModulithApplication.ModulithApplication.Subscription, "Subscription", $techn="Module", $descr="", $tags="", $link="") } -Rel(ModulithApplication.ModulithApplication.Subscription, ModulithApplication.ModulithApplication.Course, "listens to", $techn="", $tags="", $link="") Rel(ModulithApplication.ModulithApplication.Subscription, ModulithApplication.ModulithApplication.Student, "listens to", $techn="", $tags="", $link="") +Rel(ModulithApplication.ModulithApplication.Subscription, ModulithApplication.ModulithApplication.Course, "listens to", $techn="", $tags="", $link="") SHOW_LEGEND(true) @enduml \ No newline at end of file diff --git a/modulith/docs/module-subscription.puml b/modulith/docs/module-subscription.puml index f8bb4309..1ab59aad 100644 --- a/modulith/docs/module-subscription.puml +++ b/modulith/docs/module-subscription.puml @@ -14,8 +14,8 @@ Container_Boundary("ModulithApplication.ModulithApplication_boundary", "Modulith Component(ModulithApplication.ModulithApplication.Subscription, "Subscription", $techn="Module", $descr="", $tags="", $link="") } -Rel(ModulithApplication.ModulithApplication.Subscription, ModulithApplication.ModulithApplication.Course, "listens to", $techn="", $tags="", $link="") Rel(ModulithApplication.ModulithApplication.Subscription, ModulithApplication.ModulithApplication.Student, "listens to", $techn="", $tags="", $link="") +Rel(ModulithApplication.ModulithApplication.Subscription, ModulithApplication.ModulithApplication.Course, "listens to", $techn="", $tags="", $link="") SHOW_LEGEND(true) @enduml \ No newline at end of file diff --git a/web-thymeleaf-xss/README.adoc b/web-thymeleaf-xss/README.adoc new file mode 100644 index 00000000..76ec6504 --- /dev/null +++ b/web-thymeleaf-xss/README.adoc @@ -0,0 +1,124 @@ += Preventing XSS with Spring Security and Thymeleaf +:source-highlighter: highlight.js +Rashidi Zin +1.0, May 25, 2023 +:toc: +:nofooter: +:icons: font +:url-quickref: https://github.com/rashidi/spring-boot-tutorials/tree/master/web-thymeleaf-xss + +Prevent Cross-Site Scripting (XSS) attacks in Spring Boot applications using Spring Security and Thymeleaf. + +== Background + +https://owasp.org/www-community/attacks/xss/[Cross-Site Scripting (XSS)] is a security vulnerability that allows attackers to inject client-side scripts into web pages viewed by other users. This can lead to various attacks, including stealing session cookies, redirecting users to malicious websites, or performing actions on behalf of the user. + +In this tutorial, we will demonstrate how to prevent XSS attacks in a Spring Boot application using Spring Security and Thymeleaf. We will focus on two main approaches: + +1. Using Spring Security to add security headers +2. Leveraging Thymeleaf's automatic HTML escaping + +== Security Configuration + +The first line of defense against XSS attacks is to configure Spring Security to add appropriate security headers. In our example, we use the following configuration: + +[source,java] +---- +@Configuration +@EnableWebSecurity +class SecurityConfiguration { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + .headers(headers -> headers + .contentSecurityPolicy(policy -> policy.policyDirectives("default-src 'self'")) + .xssProtection(xss -> xss.headerValue(ENABLED_MODE_BLOCK)) + ) + .build(); + } +} +---- + +This configuration adds two important security headers: + +1. **Content-Security-Policy**: Restricts the sources from which content can be loaded. In this case, `default-src 'self'` means that the browser should only load resources from the same origin. + +2. **X-XSS-Protection**: Enables the browser's built-in XSS filter. The value `1; mode=block` (represented by `ENABLED_MODE_BLOCK`) tells the browser to block the response if a XSS attack is detected. + +== Thymeleaf Template + +Thymeleaf provides built-in protection against XSS by automatically escaping HTML content. Let's look at our simple greeting template: + +[source,html] +---- + + + + + Greeting + + +

+ + +---- + +The key part here is the use of `th:utext` attribute. Unlike `th:text` which automatically escapes HTML content, `th:utext` (unescaped text) renders HTML as-is. This means that if a user inputs ``, it will be executed as code rather than displayed as text. This approach is used when you need to render HTML content, but it requires careful handling of user input to prevent XSS attacks. + +== Controller Implementation + +Our controller simply takes a name parameter and passes it to the Thymeleaf template: + +[source,java] +---- +@Controller +class GreetResource { + + @GetMapping("/greet") + public String greet(@RequestParam String name, Model model) { + model.addAttribute("name", name); + + return "greet"; + } +} +---- + +== Verifying XSS Protection + +We can verify that our XSS protection is working by checking that the appropriate security headers are present in the response: + +[source,java] +---- +@WebMvcTest(controllers = GreetResource.class, includeFilters = @Filter(classes = EnableWebSecurity.class)) +class GreetResourceTests { + + @Autowired + private MockMvcTester mvc; + + @Test + @DisplayName("Given XSS protection is enabled Then response header should contain information about X-XSS-Protection and Content-Security-Policy") + void headers() { + mvc.get() + .uri("/greet?name={name}", "rashidi") + .assertThat() + .matches(status().isOk()) + .matches(header().string("Content-Security-Policy", "default-src 'self'")) + .matches(header().string("X-XSS-Protection", "1; mode=block")); + } +} +---- + +== Common XSS Vulnerabilities to Avoid + +1. **Understanding the risks of `th:utext`**: As demonstrated in our example, the `th:utext` attribute in Thymeleaf renders unescaped HTML, which can lead to XSS vulnerabilities if not handled properly. While our example uses `th:utext` for demonstration purposes, in production applications you should use `th:text` instead unless you are absolutely sure the content is safe and HTML rendering is required. + +2. **Disabling Content Security Policy**: The Content Security Policy is a powerful defense against XSS. Avoid disabling it or setting overly permissive policies. + +3. **Trusting user input**: Always validate and sanitize user input before processing it, even if you're using automatic escaping. + +== Conclusion + +In this tutorial, we've seen how to implement XSS protection in a Spring Boot application using Spring Security and Thymeleaf. We've demonstrated how to add appropriate security headers through Spring Security configuration. We've also shown how Thymeleaf handles HTML content with `th:utext`, while highlighting the potential security implications and when to use the safer `th:text` alternative instead. + +Remember that security is a multi-layered approach, and XSS protection is just one aspect of a comprehensive security strategy. Always keep your dependencies up to date and follow security best practices to ensure your application remains secure. diff --git a/web-thymeleaf-xss/src/main/resources/templates/greet.html b/web-thymeleaf-xss/src/main/resources/templates/greet.html index f7d2affd..73ee9844 100644 --- a/web-thymeleaf-xss/src/main/resources/templates/greet.html +++ b/web-thymeleaf-xss/src/main/resources/templates/greet.html @@ -5,6 +5,6 @@ Greeting -

+

\ No newline at end of file diff --git a/web-thymeleaf-xss/src/test/java/zin/rashidi/web/xss/greet/GreetResourceTests.java b/web-thymeleaf-xss/src/test/java/zin/rashidi/web/xss/greet/GreetResourceTests.java index a5cf1c73..4e6b3615 100644 --- a/web-thymeleaf-xss/src/test/java/zin/rashidi/web/xss/greet/GreetResourceTests.java +++ b/web-thymeleaf-xss/src/test/java/zin/rashidi/web/xss/greet/GreetResourceTests.java @@ -3,19 +3,18 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.test.web.servlet.assertj.MockMvcTester; -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * @author Rashidi Zin */ -@SpringBootTest(webEnvironment = RANDOM_PORT) -@AutoConfigureMockMvc +@WebMvcTest(controllers = GreetResource.class, includeFilters = @Filter(classes = EnableWebSecurity.class)) class GreetResourceTests { @Autowired