Skip to content

Commit 6728da1

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) (parse-community#8302)
1 parent 2458a8c commit 6728da1

File tree

2 files changed

+67
-12
lines changed

2 files changed

+67
-12
lines changed

spec/vulnerabilities.spec.js

+50
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

+17-12
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,7 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
6464
}
6565
}
6666

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

8069
// When the operation is complete, this.response may have several
8170
// fields.
@@ -292,6 +281,7 @@ RestWrite.prototype.runBeforeSaveTrigger = function () {
292281
delete this.data.objectId;
293282
}
294283
}
284+
this.checkProhibitedKeywords(this.data);
295285
});
296286
};
297287

@@ -1728,5 +1718,20 @@ RestWrite.prototype._updateResponseWithData = function (response, data) {
17281718
return response;
17291719
};
17301720

1721+
RestWrite.prototype.checkProhibitedKeywords = function (data) {
1722+
if (this.config.requestKeywordDenylist) {
1723+
// Scan request data for denied keywords
1724+
for (const keyword of this.config.requestKeywordDenylist) {
1725+
const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
1726+
if (match) {
1727+
throw new Parse.Error(
1728+
Parse.Error.INVALID_KEY_NAME,
1729+
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
1730+
);
1731+
}
1732+
}
1733+
}
1734+
};
1735+
17311736
export default RestWrite;
17321737
module.exports = RestWrite;

0 commit comments

Comments
 (0)