fix: Validate body field types in request middleware (#10209)

This commit is contained in:
Manuel
2026-03-15 15:41:02 +00:00
committed by GitHub
parent ad463d2e42
commit df690463f8
3 changed files with 118 additions and 4 deletions
+97
View File
@@ -430,6 +430,103 @@ describe('middlewares', () => {
expect(middlewares.checkIp(localhostV62, ['127.0.0.1'], new Map())).toBe(true);
});
describe('body field type validation', () => {
beforeEach(() => {
AppCachePut(fakeReq.body._ApplicationId, {
masterKeyIps: ['0.0.0.0/0'],
});
});
it('should reject non-string _SessionToken in body', async () => {
fakeReq.body._SessionToken = { toString: 'evil' };
await middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should reject non-string _ClientVersion in body', async () => {
fakeReq.body._ClientVersion = { toLowerCase: 'evil' };
await middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should reject non-string _InstallationId in body', async () => {
fakeReq.body._InstallationId = { toString: 'evil' };
await middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should reject non-string _ContentType in body', async () => {
fakeReq.body._ContentType = { toString: 'evil' };
await middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should reject non-string base64 in file-via-JSON upload', async () => {
fakeReq.body = Buffer.from(
JSON.stringify({
_ApplicationId: 'FakeAppId',
base64: { toString: 'evil' },
})
);
await middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should not crash the server process on non-string body fields', async () => {
// Verify that type confusion in body fields does not crash the Node.js process.
// Each request should be handled independently without affecting server stability.
const payloads = [
{ _SessionToken: { toString: 'evil' } },
{ _ClientVersion: { toLowerCase: 'evil' } },
{ _InstallationId: [1, 2, 3] },
{ _ContentType: { toString: 'evil' } },
];
for (const payload of payloads) {
const req = {
ip: '127.0.0.1',
originalUrl: 'http://example.com/parse/',
url: 'http://example.com/',
body: { _ApplicationId: 'FakeAppId', ...payload },
headers: {},
get: key => req.headers[key.toLowerCase()],
};
const res = jasmine.createSpyObj('res', ['end', 'status']);
await middlewares.handleParseHeaders(req, res);
expect(res.status).toHaveBeenCalledWith(403);
}
// Server process is still alive — a subsequent valid request works
const validReq = {
ip: '127.0.0.1',
originalUrl: 'http://example.com/parse/',
url: 'http://example.com/',
body: { _ApplicationId: 'FakeAppId' },
headers: {},
get: key => validReq.headers[key.toLowerCase()],
};
const validRes = jasmine.createSpyObj('validRes', ['end', 'status']);
let nextCalled = false;
await middlewares.handleParseHeaders(validReq, validRes, () => {
nextCalled = true;
});
expect(nextCalled).toBe(true);
expect(validRes.status).not.toHaveBeenCalled();
});
it('should still accept valid string body fields', done => {
fakeReq.body._SessionToken = 'r:validtoken';
fakeReq.body._ClientVersion = 'js1.0.0';
fakeReq.body._InstallationId = 'install123';
fakeReq.body._ContentType = 'application/json';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeReq.info.sessionToken).toEqual('r:validtoken');
expect(fakeReq.info.clientVersion).toEqual('js1.0.0');
expect(fakeReq.info.installationId).toEqual('install123');
expect(fakeReq.headers['content-type']).toEqual('application/json');
done();
});
});
});
it('should match address with cache', () => {
const ipv6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
const cache1 = new Map();
+1 -2
View File
@@ -65,8 +65,7 @@ describe('Regex Vulnerabilities', () => {
});
fail('should not work');
} catch (e) {
expect(e.data.code).toEqual(209);
expect(e.data.error).toEqual('Invalid session token');
expect(e.data.error).toEqual('unauthorized');
}
});
+20 -2
View File
@@ -150,18 +150,30 @@ export async function handleParseHeaders(req, res, next) {
// TODO: test that the REST API formats generated by the other
// SDKs are handled ok
if (req.body._ClientVersion) {
if (typeof req.body._ClientVersion !== 'string') {
return invalidRequest(req, res);
}
info.clientVersion = req.body._ClientVersion;
delete req.body._ClientVersion;
}
if (req.body._InstallationId) {
if (typeof req.body._InstallationId !== 'string') {
return invalidRequest(req, res);
}
info.installationId = req.body._InstallationId;
delete req.body._InstallationId;
}
if (req.body._SessionToken) {
if (typeof req.body._SessionToken !== 'string') {
return invalidRequest(req, res);
}
info.sessionToken = req.body._SessionToken;
delete req.body._SessionToken;
}
if (req.body._MasterKey) {
if (typeof req.body._MasterKey !== 'string') {
return invalidRequest(req, res);
}
info.masterKey = req.body._MasterKey;
delete req.body._MasterKey;
}
@@ -181,6 +193,9 @@ export async function handleParseHeaders(req, res, next) {
delete req.body._context;
}
if (req.body._ContentType) {
if (typeof req.body._ContentType !== 'string') {
return invalidRequest(req, res);
}
req.headers['content-type'] = req.body._ContentType;
delete req.body._ContentType;
}
@@ -190,14 +205,17 @@ export async function handleParseHeaders(req, res, next) {
}
if (info.sessionToken && typeof info.sessionToken !== 'string') {
info.sessionToken = info.sessionToken.toString();
return invalidRequest(req, res);
}
if (info.clientVersion) {
if (info.clientVersion && typeof info.clientVersion === 'string') {
info.clientSDK = ClientSDK.fromString(info.clientVersion);
}
if (fileViaJSON && req.body) {
if (req.body.base64 && typeof req.body.base64 !== 'string') {
return invalidRequest(req, res);
}
req.fileData = req.body.fileData;
// We need to repopulate req.body with a buffer
var base64 = req.body.base64;