Skip to content

Commit 71fba5f

Browse files
committed
feat: add response argument to cloud functions for HTTP status and headers
1 parent 4cdda82 commit 71fba5f

File tree

3 files changed

+110
-5
lines changed

3 files changed

+110
-5
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ A big _thank you_ 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
7777
- [Reserved Keys](#reserved-keys)
7878
- [Parameters](#parameters-1)
7979
- [Logging](#logging)
80+
- [Cloud Function Custom HTTP Response](#cloud-functions-http-response)
8081
- [Deprecations](#deprecations)
8182
- [Live Query](#live-query)
8283
- [GraphQL](#graphql)

spec/CloudCode.spec.js

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ describe('Cloud Code', () => {
228228
expect(response.headers['x-custom-header']).toEqual('third');
229229
});
230230

231-
it('res.status() throws error for non-number status code', async () => {
231+
it('res.status() throws error for non-integer status code', async () => {
232232
Parse.Cloud.define('invalidStatusType', (req, res) => {
233233
res.status('200');
234234
return { message: 'ok' };
@@ -251,6 +251,29 @@ describe('Cloud Code', () => {
251251
}
252252
});
253253

254+
it('res.status() throws error for NaN status code', async () => {
255+
Parse.Cloud.define('nanStatus', (req, res) => {
256+
res.status(NaN);
257+
return { message: 'ok' };
258+
});
259+
260+
try {
261+
await request({
262+
method: 'POST',
263+
url: 'http://localhost:8378/1/functions/nanStatus',
264+
headers: {
265+
'X-Parse-Application-Id': 'test',
266+
'X-Parse-REST-API-Key': 'rest',
267+
'Content-Type': 'application/json',
268+
},
269+
body: {},
270+
});
271+
fail('Expected request to fail');
272+
} catch (response) {
273+
expect(response.status).toEqual(400);
274+
}
275+
});
276+
254277
it('res.set() throws error for non-string header name', async () => {
255278
Parse.Cloud.define('invalidHeaderName', (req, res) => {
256279
res.set(123, 'value');
@@ -320,6 +343,75 @@ describe('Cloud Code', () => {
320343
}
321344
});
322345

346+
it('res.set() throws error for empty header name', async () => {
347+
Parse.Cloud.define('emptyHeaderName', (req, res) => {
348+
res.set(' ', 'value');
349+
return { message: 'ok' };
350+
});
351+
352+
try {
353+
await request({
354+
method: 'POST',
355+
url: 'http://localhost:8378/1/functions/emptyHeaderName',
356+
headers: {
357+
'X-Parse-Application-Id': 'test',
358+
'X-Parse-REST-API-Key': 'rest',
359+
'Content-Type': 'application/json',
360+
},
361+
body: {},
362+
});
363+
fail('Expected request to fail');
364+
} catch (response) {
365+
expect(response.status).toEqual(400);
366+
}
367+
});
368+
369+
it('res.set() throws error for prototype pollution header names', async () => {
370+
Parse.Cloud.define('protoHeaderName', (req, res) => {
371+
res.set('__proto__', 'value');
372+
return { message: 'ok' };
373+
});
374+
375+
try {
376+
await request({
377+
method: 'POST',
378+
url: 'http://localhost:8378/1/functions/protoHeaderName',
379+
headers: {
380+
'X-Parse-Application-Id': 'test',
381+
'X-Parse-REST-API-Key': 'rest',
382+
'Content-Type': 'application/json',
383+
},
384+
body: {},
385+
});
386+
fail('Expected request to fail');
387+
} catch (response) {
388+
expect(response.status).toEqual(400);
389+
}
390+
});
391+
392+
it('res.set() throws error for CRLF in header value', async () => {
393+
Parse.Cloud.define('crlfHeaderValue', (req, res) => {
394+
res.set('X-Custom-Header', 'value\r\nX-Injected: bad');
395+
return { message: 'ok' };
396+
});
397+
398+
try {
399+
await request({
400+
method: 'POST',
401+
url: 'http://localhost:8378/1/functions/crlfHeaderValue',
402+
headers: {
403+
'X-Parse-Application-Id': 'test',
404+
'X-Parse-REST-API-Key': 'rest',
405+
'Content-Type': 'application/json',
406+
},
407+
body: {},
408+
});
409+
fail('Expected request to fail');
410+
} catch (response) {
411+
expect(response.status).toEqual(400);
412+
}
413+
});
414+
323415
it('can get config', () => {
324416
const config = Parse.Server;
325417
let currentConfig = Config.get('test');

src/Routers/FunctionsRouter.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ import { logger } from '../logger';
1212
class CloudResponse {
1313
constructor() {
1414
this._status = null;
15-
this._headers = {};
15+
this._headers = Object.create(null);
1616
}
1717

1818
status(code) {
19-
if (typeof code !== 'number') {
20-
throw new Error('Status code must be a number');
19+
if (!Number.isInteger(code)) {
20+
throw new Error('Status code must be an integer');
2121
}
2222
if (code < 100 || code > 599) {
2323
throw new Error('Status code must be between 100 and 599');
@@ -30,10 +30,22 @@ class CloudResponse {
3030
if (typeof name !== 'string') {
3131
throw new Error('Header name must be a string');
3232
}
33+
const headerName = name.trim();
34+
if (!headerName) {
35+
throw new Error('Header name must not be empty');
36+
}
37+
if (headerName === '__proto__' || headerName === 'constructor' || headerName === 'prototype') {
38+
throw new Error('Invalid header name');
39+
}
3340
if (value === undefined || value === null) {
3441
throw new Error('Header value must be defined');
3542
}
36-
this._headers[name] = value;
43+
const headerValue = Array.isArray(value) ? value.map(v => String(v)) : String(value);
44+
const values = Array.isArray(headerValue) ? headerValue : [headerValue];
45+
if (values.some(v => /[\r\n]/.test(v))) {
46+
throw new Error('Header value must not contain CRLF');
47+
}
48+
this._headers[headerName] = headerValue;
3749
return this;
3850
}
3951

0 commit comments

Comments
 (0)