Finding an Authorization Bypass on my Own Website

We discussed this vulnerability during Episode 125 on 07 March 2022

Permissive parsing strikes again, MySQLjs by would accept objects as values for a parameterized query with a somewhat surprising default behaviour. The key issue here though is that MySQLjs exposes an interface entirely like prepared statements, but is actually crafting the query on the client side rather than using server-side prepared statements.

Query:            SELECT * FROM users WHERE ?
Parameter Value:  {"password": "example"}
Results in:       SELECT * FROM users WHERE `password` = "example"

In this example it kinda makes sense, passing in an object for a more complex replacement, but it also supports that replace when you would expect a simple value to be injected.

Query:            SELECT * FROM example WHERE some_column = ?
Parameter Value:  {"some_column": 1}
Results in:       SELECT * FROM example WHERE some_column = `some_column` = 1

Its this second example, that is likely to occur, yet might introduce a vulnerability. Looking at that WHERE condition this breaks down into two comparisions some_column = some_column which will resolve to true since both are pointing to the same column, that is then compared with 1 which is a “truthy” value.

Maxwell Dulin encounted this within his blog’s authentication system:

async viewUserInfo(user,password){
		//prepared statements prevent SQL injections
		var query = "SELECT * FROM Users where username = ? AND password = ?";
		const [rows, fields]  = await pool.query(query, [user,password]);
		return rows;
}

For this code, the user value is passed in directly, but the password will be hashed first, so it cannot be an unexpected object, but the user can be. Which is exactly what the attack was.

{
  "username": {
    "username" : 1
  },
  "password":"<some_password>"
}

The query in this case will resolve to:

SELECT * From Users WHERE username = `username` = 1 AND password = <some-hash>

Leading to the first condition (username) being true for all rows in the database, and then as long as one user has the password the second condition will also be true and the login will proceed.