diff --git a/README.adoc b/README.adoc index 3cc3b65..76af589 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 a8cd915..86d3a36 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 0000000..bb387e2 --- /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 001e328..98a6ec7 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 f8bb430..1ab59aa 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/.gitattributes b/web-thymeleaf-xss/.gitattributes new file mode 100644 index 0000000..8af972c --- /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 0000000..c2065bc --- /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/README.adoc b/web-thymeleaf-xss/README.adoc new file mode 100644 index 0000000..76ec650 --- /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/build.gradle.kts b/web-thymeleaf-xss/build.gradle.kts new file mode 100644 index 0000000..4ebac0e --- /dev/null +++ b/web-thymeleaf-xss/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + java + id("org.springframework.boot") version "3.5.0" + 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 0000000..ad0e708 --- /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 0000000..46cde7d --- /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 0000000..75bc9b3 --- /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 0000000..06932bb --- /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 0000000..84ddb44 --- /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 0000000..73ee984 --- /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 0000000..633fac4 --- /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 0000000..4e6b361 --- /dev/null +++ b/web-thymeleaf-xss/src/test/java/zin/rashidi/web/xss/greet/GreetResourceTests.java @@ -0,0 +1,34 @@ +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.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.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Rashidi Zin + */ +@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")); + } + +} \ No newline at end of file