Skip to content

Commit d9c3c02

Browse files
authored
refactor: Parse Server option requestKeywordDenylist can be bypassed via Cloud Code Webhooks or Triggers; fixes security vulnerability [GHSA-xprv-wvh7-qqqx](GHSA-xprv-wvh7-qqqx) (#8303)
1 parent 46dbecd commit d9c3c02

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
@@ -65,18 +65,7 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
6565
}
6666
}
6767

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

8170
// When the operation is complete, this.response may have several
8271
// fields.
@@ -293,6 +282,7 @@ RestWrite.prototype.runBeforeSaveTrigger = function () {
293282
delete this.data.objectId;
294283
}
295284
}
285+
this.checkProhibitedKeywords(this.data);
296286
});
297287
};
298288

@@ -1735,5 +1725,20 @@ RestWrite.prototype._updateResponseWithData = function (response, data) {
17351725
return response;
17361726
};
17371727

1728+
RestWrite.prototype.checkProhibitedKeywords = function (data) {
1729+
if (this.config.requestKeywordDenylist) {
1730+
// Scan request data for denied keywords
1731+
for (const keyword of this.config.requestKeywordDenylist) {
1732+
const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
1733+
if (match) {
1734+
throw new Parse.Error(
1735+
Parse.Error.INVALID_KEY_NAME,
1736+
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
1737+
);
1738+
}
1739+
}
1740+
}
1741+
};
1742+
17381743
export default RestWrite;
17391744
module.exports = RestWrite;

0 commit comments

Comments
 (0)