Skip to content

Commit df69046

Browse files
authored
fix: Validate body field types in request middleware (#10209)
1 parent ad463d2 commit df69046

File tree

3 files changed

+118
-4
lines changed

3 files changed

+118
-4
lines changed

spec/Middlewares.spec.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,103 @@ describe('middlewares', () => {
430430
expect(middlewares.checkIp(localhostV62, ['127.0.0.1'], new Map())).toBe(true);
431431
});
432432

433+
describe('body field type validation', () => {
434+
beforeEach(() => {
435+
AppCachePut(fakeReq.body._ApplicationId, {
436+
masterKeyIps: ['0.0.0.0/0'],
437+
});
438+
});
439+
440+
it('should reject non-string _SessionToken in body', async () => {
441+
fakeReq.body._SessionToken = { toString: 'evil' };
442+
await middlewares.handleParseHeaders(fakeReq, fakeRes);
443+
expect(fakeRes.status).toHaveBeenCalledWith(403);
444+
});
445+
446+
it('should reject non-string _ClientVersion in body', async () => {
447+
fakeReq.body._ClientVersion = { toLowerCase: 'evil' };
448+
await middlewares.handleParseHeaders(fakeReq, fakeRes);
449+
expect(fakeRes.status).toHaveBeenCalledWith(403);
450+
});
451+
452+
it('should reject non-string _InstallationId in body', async () => {
453+
fakeReq.body._InstallationId = { toString: 'evil' };
454+
await middlewares.handleParseHeaders(fakeReq, fakeRes);
455+
expect(fakeRes.status).toHaveBeenCalledWith(403);
456+
});
457+
458+
it('should reject non-string _ContentType in body', async () => {
459+
fakeReq.body._ContentType = { toString: 'evil' };
460+
await middlewares.handleParseHeaders(fakeReq, fakeRes);
461+
expect(fakeRes.status).toHaveBeenCalledWith(403);
462+
});
463+
464+
it('should reject non-string base64 in file-via-JSON upload', async () => {
465+
fakeReq.body = Buffer.from(
466+
JSON.stringify({
467+
_ApplicationId: 'FakeAppId',
468+
base64: { toString: 'evil' },
469+
})
470+
);
471+
await middlewares.handleParseHeaders(fakeReq, fakeRes);
472+
expect(fakeRes.status).toHaveBeenCalledWith(403);
473+
});
474+
475+
it('should not crash the server process on non-string body fields', async () => {
476+
// Verify that type confusion in body fields does not crash the Node.js process.
477+
// Each request should be handled independently without affecting server stability.
478+
const payloads = [
479+
{ _SessionToken: { toString: 'evil' } },
480+
{ _ClientVersion: { toLowerCase: 'evil' } },
481+
{ _InstallationId: [1, 2, 3] },
482+
{ _ContentType: { toString: 'evil' } },
483+
];
484+
for (const payload of payloads) {
485+
const req = {
486+
ip: '127.0.0.1',
487+
originalUrl: 'http://example.com/parse/',
488+
url: 'http://example.com/',
489+
body: { _ApplicationId: 'FakeAppId', ...payload },
490+
headers: {},
491+
get: key => req.headers[key.toLowerCase()],
492+
};
493+
const res = jasmine.createSpyObj('res', ['end', 'status']);
494+
await middlewares.handleParseHeaders(req, res);
495+
expect(res.status).toHaveBeenCalledWith(403);
496+
}
497+
// Server process is still alive — a subsequent valid request works
498+
const validReq = {
499+
ip: '127.0.0.1',
500+
originalUrl: 'http://example.com/parse/',
501+
url: 'http://example.com/',
502+
body: { _ApplicationId: 'FakeAppId' },
503+
headers: {},
504+
get: key => validReq.headers[key.toLowerCase()],
505+
};
506+
const validRes = jasmine.createSpyObj('validRes', ['end', 'status']);
507+
let nextCalled = false;
508+
await middlewares.handleParseHeaders(validReq, validRes, () => {
509+
nextCalled = true;
510+
});
511+
expect(nextCalled).toBe(true);
512+
expect(validRes.status).not.toHaveBeenCalled();
513+
});
514+
515+
it('should still accept valid string body fields', done => {
516+
fakeReq.body._SessionToken = 'r:validtoken';
517+
fakeReq.body._ClientVersion = 'js1.0.0';
518+
fakeReq.body._InstallationId = 'install123';
519+
fakeReq.body._ContentType = 'application/json';
520+
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
521+
expect(fakeReq.info.sessionToken).toEqual('r:validtoken');
522+
expect(fakeReq.info.clientVersion).toEqual('js1.0.0');
523+
expect(fakeReq.info.installationId).toEqual('install123');
524+
expect(fakeReq.headers['content-type']).toEqual('application/json');
525+
done();
526+
});
527+
});
528+
});
529+
433530
it('should match address with cache', () => {
434531
const ipv6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
435532
const cache1 = new Map();

spec/RegexVulnerabilities.spec.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,7 @@ describe('Regex Vulnerabilities', () => {
6565
});
6666
fail('should not work');
6767
} catch (e) {
68-
expect(e.data.code).toEqual(209);
69-
expect(e.data.error).toEqual('Invalid session token');
68+
expect(e.data.error).toEqual('unauthorized');
7069
}
7170
});
7271

src/middlewares.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,18 +150,30 @@ export async function handleParseHeaders(req, res, next) {
150150
// TODO: test that the REST API formats generated by the other
151151
// SDKs are handled ok
152152
if (req.body._ClientVersion) {
153+
if (typeof req.body._ClientVersion !== 'string') {
154+
return invalidRequest(req, res);
155+
}
153156
info.clientVersion = req.body._ClientVersion;
154157
delete req.body._ClientVersion;
155158
}
156159
if (req.body._InstallationId) {
160+
if (typeof req.body._InstallationId !== 'string') {
161+
return invalidRequest(req, res);
162+
}
157163
info.installationId = req.body._InstallationId;
158164
delete req.body._InstallationId;
159165
}
160166
if (req.body._SessionToken) {
167+
if (typeof req.body._SessionToken !== 'string') {
168+
return invalidRequest(req, res);
169+
}
161170
info.sessionToken = req.body._SessionToken;
162171
delete req.body._SessionToken;
163172
}
164173
if (req.body._MasterKey) {
174+
if (typeof req.body._MasterKey !== 'string') {
175+
return invalidRequest(req, res);
176+
}
165177
info.masterKey = req.body._MasterKey;
166178
delete req.body._MasterKey;
167179
}
@@ -181,6 +193,9 @@ export async function handleParseHeaders(req, res, next) {
181193
delete req.body._context;
182194
}
183195
if (req.body._ContentType) {
196+
if (typeof req.body._ContentType !== 'string') {
197+
return invalidRequest(req, res);
198+
}
184199
req.headers['content-type'] = req.body._ContentType;
185200
delete req.body._ContentType;
186201
}
@@ -190,14 +205,17 @@ export async function handleParseHeaders(req, res, next) {
190205
}
191206

192207
if (info.sessionToken && typeof info.sessionToken !== 'string') {
193-
info.sessionToken = info.sessionToken.toString();
208+
return invalidRequest(req, res);
194209
}
195210

196-
if (info.clientVersion) {
211+
if (info.clientVersion && typeof info.clientVersion === 'string') {
197212
info.clientSDK = ClientSDK.fromString(info.clientVersion);
198213
}
199214

200215
if (fileViaJSON && req.body) {
216+
if (req.body.base64 && typeof req.body.base64 !== 'string') {
217+
return invalidRequest(req, res);
218+
}
201219
req.fileData = req.body.fileData;
202220
// We need to repopulate req.body with a buffer
203221
var base64 = req.body.base64;

0 commit comments

Comments
 (0)