Skip to content

Commit 0a2d412

Browse files
authored
fix: Parse Server option requestKeywordDenylist can be bypassed via Cloud Code Webhooks or Triggers; fixes security vulnerability [GHSA-xprv-wvh7-qqqx](GHSA-xprv-wvh7-qqqx) (#8301)
1 parent 3534652 commit 0a2d412

File tree

2 files changed

+67
-12
lines changed

2 files changed

+67
-12
lines changed

spec/vulnerabilities.spec.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,56 @@ describe('Vulnerabilities', () => {
109109
);
110110
});
111111

112+
it('denies creating a cloud trigger with polluted data', async () => {
113+
Parse.Cloud.beforeSave('TestObject', ({ object }) => {
114+
object.set('obj', {
115+
constructor: {
116+
prototype: {
117+
dummy: 0,
118+
},
119+
},
120+
});
121+
});
122+
await expectAsync(new Parse.Object('TestObject').save()).toBeRejectedWith(
123+
new Parse.Error(
124+
Parse.Error.INVALID_KEY_NAME,
125+
'Prohibited keyword in request data: {"key":"constructor"}.'
126+
)
127+
);
128+
});
129+
130+
it('denies creating a hook with polluted data', async () => {
131+
const express = require('express');
132+
const bodyParser = require('body-parser');
133+
const port = 34567;
134+
const hookServerURL = 'http://localhost:' + port;
135+
const app = express();
136+
app.use(bodyParser.json({ type: '*/*' }));
137+
const server = await new Promise(resolve => {
138+
const res = app.listen(port, undefined, () => resolve(res));
139+
});
140+
app.post('/BeforeSave', function (req, res) {
141+
const object = Parse.Object.fromJSON(req.body.object);
142+
object.set('hello', 'world');
143+
object.set('obj', {
144+
constructor: {
145+
prototype: {
146+
dummy: 0,
147+
},
148+
},
149+
});
150+
res.json({ success: object });
151+
});
152+
await Parse.Hooks.createTrigger('TestObject', 'beforeSave', hookServerURL + '/BeforeSave');
153+
await expectAsync(new Parse.Object('TestObject').save()).toBeRejectedWith(
154+
new Parse.Error(
155+
Parse.Error.INVALID_KEY_NAME,
156+
'Prohibited keyword in request data: {"key":"constructor"}.'
157+
)
158+
);
159+
await new Promise(resolve => server.close(resolve));
160+
});
161+
112162
it('allows BSON type code data in write request with custom denylist', async () => {
113163
await reconfigureServer({
114164
requestKeywordDenylist: [],

src/RestWrite.js

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,7 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
6262
}
6363
}
6464

65-
if (this.config.requestKeywordDenylist) {
66-
// Scan request data for denied keywords
67-
for (const keyword of this.config.requestKeywordDenylist) {
68-
const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
69-
if (match) {
70-
throw new Parse.Error(
71-
Parse.Error.INVALID_KEY_NAME,
72-
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
73-
);
74-
}
75-
}
76-
}
65+
this.checkProhibitedKeywords(data);
7766

7867
// When the operation is complete, this.response may have several
7968
// fields.
@@ -296,6 +285,7 @@ RestWrite.prototype.runBeforeSaveTrigger = function () {
296285
delete this.data.objectId;
297286
}
298287
}
288+
this.checkProhibitedKeywords(this.data);
299289
});
300290
};
301291

@@ -1651,5 +1641,20 @@ RestWrite.prototype._updateResponseWithData = function (response, data) {
16511641
return response;
16521642
};
16531643

1644+
RestWrite.prototype.checkProhibitedKeywords = function (data) {
1645+
if (this.config.requestKeywordDenylist) {
1646+
// Scan request data for denied keywords
1647+
for (const keyword of this.config.requestKeywordDenylist) {
1648+
const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
1649+
if (match) {
1650+
throw new Parse.Error(
1651+
Parse.Error.INVALID_KEY_NAME,
1652+
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
1653+
);
1654+
}
1655+
}
1656+
}
1657+
};
1658+
16541659
export default RestWrite;
16551660
module.exports = RestWrite;

0 commit comments

Comments
 (0)