Tags:
As someone who spends half of their year teaching web application security, I tend to give a lot of presentations that include live demonstrations, mitigation techniques, and exploits. When preparing for a quality assurance presentation earlier this year, I decided to show the group a demonstration of Cross-Site Request Forgery (CSRF) and how to fix the vulnerability.
A CSRF Refresher
If you're not familiar with Cross-Site Request Forgery (CSRF), check out the article Steve Kosten wrote earlier this year about the attack, how it works, and how to defend your applications using synchronization tokens:
The Demo
My favorite way to demonstrate the power of CSRF is by exploiting a vulnerable change password screen to take over a user's account. The attack goes like this:
- Our unsuspecting user signs into a web site, opens a new tab, and visits my evil web page
- My evil web page submits a forged request
- The server changes the user's password to a value that I know, and
- I own their account!
I know, I know. Change password screens are supposed to ask for the user's previous password, and the attacker wouldn't know what that is! Well, you're absolutely right. But, you'd be shocked at the number of web applications that I test each year that are missing this simple security mechanism.
Now, let's create our vulnerable page.
The Change Password Page
My demo applications in the Java world use Spring MVC, Spring Security, Thymeleaf, and Spring JPA. If you're not familiar with Thymeleaf, see the References section, and check out the documentation. This framework provides an easy-to-use HTML template engine that allows us to quickly create views for a web application. I created the following page called changepassword.html containing inputs for new password and confirm password, and a submit button:
<form method="post" th:action="@{/content/changepassword}" th:object="${changePasswordForm}"> <table cellpadding="0"> <tbody><tr> <td align="right">New Password:</td> <td> <input id="newPassword" type="password" th:field="*{newPassword}"> </td> </tr> <tr> <td align="right">Confirm Password:</td> <td> <input id="confirmPassword" type="password" th:field="*{confirmPassword}"> </td> </tr> <tr> <td> <input type="submit" value="Change Password" id="btnChangePassword"> </td> </tr> </tbody></table>
The Change Password HTML
After creating the server-side controller to change the user's password, I fired up the application, signed into my demo site, and browsed to the change password screen. In the HTML source I found something very interesting. A mysterious hidden field, "_csrf", had been added to my form:
<input type="hidden" name="_csrf" value="aa1688de-3984-448a-a56e-43bfa027790c" />
When submitting the request to change my password, the raw HTTP request sent the _csrf request parameter to the server and I received a successful 200 response code.
POST /csrf/content/changepassword HTTP/1.1 Host: localhost:8080 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Cookie: JSESSIONID=2A801D28758DFF67FCDEC7BE180857B8 Connection: keep-alive Content-Type: application/x-www-form-urlencoded newPassword=Stapler&confirmPassword=Stapler&_csrf=bd729b66-8d90-46c8-94ff-136cd1188caa
"Interesting," I thought to myself. Is this token really being validated on the server-side? I immediately fired up Burp Suite, captured a change password request, and sent it to the repeater plug-in.
For the first test, I removed the "_csrf" parameter, submitted the request, and watched it fail.
POST /csrf/content/changepassword HTTP/1.1 Host: localhost:8080 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Cookie: JSESSIONID=2A801D28758DFF67FCDEC7BE180857B8 Connection: keep-alive Content-Type: application/x-www-form-urlencoded newPassword=Stapler&confirmPassword=Stapler
For the second test, I tried again with an invalid "_csrf" token.
POST /csrf/content/changepassword HTTP/1.1 Host: localhost:8080 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Cookie: JSESSIONID=2A801D28758DFF67FCDEC7BE180857B8 Connection: keep-alive Content-Type: application/x-www-form-urlencoded newPassword=Stapler&confirmPassword=Stapler&_csrf=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
Both test cases returned the same 403 Forbidden HTTP response code:
HTTP/1.1 403 Forbidden Server: Apache-Coyote/1.1 Cache-Control: no-cache, no-store, max-age=0, must-revalidate Content-Type: text/html;charset=UTF-8 Content-Language: en-US Content-Length: 2464 Date: Thu, 27 Aug 2015 11:48:08 GMT
Spring Security
As an attacker trying to show how powerful CSRF attacks can be, this is troubling. As a defender, this tells me that my application's POST requests are protected from CSRF attacks by default! According to the Spring documentation, CSRF protection has been baked into Spring Security since v3.2.0 was released in August 2013. For applications using the Java configuration, this is enabled by default and explains why my change password demo page was already being protected.
However, applications using XML-based configuration on versions before Spring v4.0 still need to enable the feature in the http configuration:
<http> ... <csrf> </csrf>
Applications on Spring v4.0+ have this protection enabled by default, regardless of how they are configured, making it easy for code reviewers and static analyzers to find instances where it is disabled. Of course this is exactly what I had to do for my presentation demo to work, but it's a security anti-pattern so we don't discuss how to do this here. See the Reference section for more details on Spring's CSRF protections.
Caveats
Before we run around the office saying our applications are safe because we're using Thymeleaf and Spring Security, I'm going to identify a few weaknesses in this defense.
- Thymeleaf and Spring Security only place CSRF tokens into "form" elements. This means that only form actions submitting POST parameters are protected. If your application makes data modifications via GET requests or AJAX calls, you still have work to do.
- Cross-Site Scripting (XSS) attacks can easily extract CSRF tokens to an attacker's remote web server. If you have XSS vulnerabilities in your application, this protection does you no good.
- My demo application is using Thymeleaf 2.1.3 and Spring 4.1.3. Older versions of these frameworks may not provide the same protection. As always, make sure you perform code reviews and security testing on your applications to ensure they are secure before deploying them to production.
References:
- http://www.thymeleaf.org/
- http://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html
To learn more about securing your Java applications, sign up for DEV541: Secure Coding in Java/JEE!
Eric Johnson (Twitter: @emjohn20) is a Senior Security Consultant at Cypress Data Defense, Application Security Curriculum Product Manager at SANS, and a certified SANS instructor. He is the lead author and instructor for DEV544 Secure Coding in .NET, as well as an instructor for DEV541 Secure Coding in Java/JEE. Eric serves on the advisory board for the SANS Securing the Human Developer awareness training program and is a contributing author for the developer security awareness modules. Eric's previous experience includes web and mobile application penetration testing, secure code review, risk assessment, static source code analysis, security research, and developing security tools. He completed a bachelor of science in computer engineering and a master of science in information assurance at Iowa State University, and currently holds the CISSP, GWAPT, GSSP-.NET, and GSSP-Java certifications.