fix: Security upgrade to parse 7.0.1 (#9877)

This commit is contained in:
Antoine Cormouls
2025-10-15 18:39:37 +02:00
committed by GitHub
parent 84cebd439e
commit abfa94cd6d
10 changed files with 230 additions and 91 deletions

View File

@@ -89,12 +89,16 @@ describe('LinkedInAdapter', function () {
describe('Test getUserFromAccessToken', function () {
it('should fetch user successfully', async function () {
global.fetch = jasmine.createSpy().and.returnValue(
Promise.resolve({
ok: true,
json: () => Promise.resolve({ id: 'validUserId' }),
})
);
mockFetch([
{
url: 'https://api.linkedin.com/v2/me',
method: 'GET',
response: {
ok: true,
json: () => Promise.resolve({ id: 'validUserId' }),
},
},
]);
const user = await adapter.getUserFromAccessToken('validToken', false);
@@ -104,14 +108,21 @@ describe('LinkedInAdapter', function () {
'x-li-format': 'json',
'x-li-src': undefined,
},
method: 'GET',
});
expect(user).toEqual({ id: 'validUserId' });
});
it('should throw error for invalid response', async function () {
global.fetch = jasmine.createSpy().and.returnValue(
Promise.resolve({ ok: false })
);
mockFetch([
{
url: 'https://api.linkedin.com/v2/me',
method: 'GET',
response: {
ok: false,
},
},
]);
await expectAsync(adapter.getUserFromAccessToken('invalidToken', false)).toBeRejectedWith(
new Error('LinkedIn API request failed.')
@@ -121,12 +132,16 @@ describe('LinkedInAdapter', function () {
describe('Test getAccessTokenFromCode', function () {
it('should fetch token successfully', async function () {
global.fetch = jasmine.createSpy().and.returnValue(
Promise.resolve({
ok: true,
json: () => Promise.resolve({ access_token: 'validToken' }),
})
);
mockFetch([
{
url: 'https://www.linkedin.com/oauth/v2/accessToken',
method: 'POST',
response: {
ok: true,
json: () => Promise.resolve({ access_token: 'validToken' }),
},
},
]);
const tokenResponse = await adapter.getAccessTokenFromCode('validCode', 'http://example.com');
@@ -139,9 +154,15 @@ describe('LinkedInAdapter', function () {
});
it('should throw error for invalid response', async function () {
global.fetch = jasmine.createSpy().and.returnValue(
Promise.resolve({ ok: false })
);
mockFetch([
{
url: 'https://www.linkedin.com/oauth/v2/accessToken',
method: 'POST',
response: {
ok: false,
},
},
]);
await expectAsync(
adapter.getAccessTokenFromCode('invalidCode', 'http://example.com')

View File

@@ -23,7 +23,8 @@ describe('WeChatAdapter', function () {
const user = await adapter.getUserFromAccessToken('validToken', { id: 'validOpenId' });
expect(global.fetch).toHaveBeenCalledWith(
'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId'
'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId',
jasmine.any(Object)
);
expect(user).toEqual({ errcode: 0, id: 'validUserId' });
});
@@ -64,7 +65,8 @@ describe('WeChatAdapter', function () {
const token = await adapter.getAccessTokenFromCode(authData);
expect(global.fetch).toHaveBeenCalledWith(
'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code'
'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
jasmine.any(Object)
);
expect(token).toEqual('validToken');
});

View File

@@ -189,7 +189,7 @@ describe('Cloud Code Logger', () => {
});
});
it_id('8088de8a-7cba-4035-8b05-4a903307e674')(it)('should log cloud function execution using the custom log level', async done => {
it_id('8088de8a-7cba-4035-8b05-4a903307e674')(it)('should log cloud function execution using the custom log level', async () => {
Parse.Cloud.define('aFunction', () => {
return 'it worked!';
});
@@ -203,6 +203,7 @@ describe('Cloud Code Logger', () => {
expect(log).toEqual('info');
});
Parse.Cloud._removeAllHooks();
await reconfigureServer({
silent: true,
logLevels: {
@@ -211,6 +212,10 @@ describe('Cloud Code Logger', () => {
},
});
Parse.Cloud.define('bFunction', () => {
throw new Error('Failed');
});
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
try {
@@ -221,15 +226,12 @@ describe('Cloud Code Logger', () => {
.allArgs()
.find(log => log[1].startsWith('Failed running cloud function bFunction for '))?.[0];
expect(log).toEqual('info');
done();
}
});
it('should log cloud function triggers using the custom log level', async () => {
Parse.Cloud.beforeSave('TestClass', () => {});
Parse.Cloud.afterSave('TestClass', () => {});
const execTest = async (logLevel, triggerBeforeSuccess, triggerAfter) => {
Parse.Cloud._removeAllHooks();
await reconfigureServer({
silent: true,
logLevel,
@@ -239,6 +241,9 @@ describe('Cloud Code Logger', () => {
},
});
Parse.Cloud.beforeSave('TestClass', () => { });
Parse.Cloud.afterSave('TestClass', () => { });
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
const obj = new Parse.Object('TestClass');
await obj.save();
@@ -344,6 +349,7 @@ describe('Cloud Code Logger', () => {
});
it('should log cloud function execution using the silent log level', async () => {
Parse.Cloud._removeAllHooks();
await reconfigureServer({
logLevels: {
cloudFunctionSuccess: 'silent',
@@ -367,6 +373,7 @@ describe('Cloud Code Logger', () => {
});
it('should log cloud function triggers using the silent log level', async () => {
Parse.Cloud._removeAllHooks();
await reconfigureServer({
logLevels: {
triggerAfter: 'silent',

View File

@@ -1395,10 +1395,10 @@ describe('Parse.Object testing', () => {
.save()
.then(function () {
const query = new Parse.Query(TestObject);
return query.find(object.id);
return query.get(object.id);
})
.then(function (results) {
updatedObject = results[0];
.then(function (result) {
updatedObject = result;
updatedObject.set('x', 11);
return updatedObject.save();
})
@@ -1409,7 +1409,8 @@ describe('Parse.Object testing', () => {
equal(object.createdAt.getTime(), updatedObject.createdAt.getTime());
equal(object.updatedAt.getTime(), updatedObject.updatedAt.getTime());
done();
});
})
.catch(done.fail);
});
xit('fetchAll backbone-style callbacks', function (done) {

View File

@@ -46,7 +46,7 @@ describe_only_db('mongo')('Parse.Query with comment testing', () => {
});
it('send comment with query through REST', async () => {
const comment = 'Hello Parse';
const comment = `Hello Parse ${Date.now()}`;
const object = new TestObject();
object.set('name', 'object');
await object.save();
@@ -58,23 +58,55 @@ describe_only_db('mongo')('Parse.Query with comment testing', () => {
},
});
await request(options);
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
// Wait for profile entry to appear with retry logic
let result;
const maxRetries = 10;
const retryDelay = 100;
for (let i = 0; i < maxRetries; i++) {
result = await database.collection('system.profile').findOne(
{ 'command.explain.comment': comment },
{ sort: { ts: -1 } }
);
if (result) {
break;
}
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
expect(result).toBeDefined();
expect(result.command.explain.comment).toBe(comment);
});
it('send comment with query', async () => {
const comment = 'Hello Parse';
const comment = `Hello Parse ${Date.now()}`;
const object = new TestObject();
object.set('name', 'object');
await object.save();
const collection = await config.database.adapter._adaptiveCollection('TestObject');
await collection._rawFind({ name: 'object' }, { comment: comment });
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
// Wait for profile entry to appear with retry logic
let result;
const maxRetries = 10;
const retryDelay = 100;
for (let i = 0; i < maxRetries; i++) {
result = await database.collection('system.profile').findOne(
{ 'command.comment': comment },
{ sort: { ts: -1 } }
);
if (result) {
break;
}
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
expect(result).toBeDefined();
expect(result.command.comment).toBe(comment);
});
it('send a comment with a count query', async () => {
const comment = 'Hello Parse';
const comment = `Hello Parse ${Date.now()}`;
const object = new TestObject();
object.set('name', 'object');
await object.save();
@@ -86,12 +118,28 @@ describe_only_db('mongo')('Parse.Query with comment testing', () => {
const collection = await config.database.adapter._adaptiveCollection('TestObject');
const countResult = await collection.count({ name: 'object' }, { comment: comment });
expect(countResult).toEqual(2);
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
// Wait for profile entry to appear with retry logic
let result;
const maxRetries = 10;
const retryDelay = 100;
for (let i = 0; i < maxRetries; i++) {
result = await database.collection('system.profile').findOne(
{ 'command.comment': comment },
{ sort: { ts: -1 } }
);
if (result) {
break;
}
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
expect(result).toBeDefined();
expect(result.command.comment).toBe(comment);
});
it('attach a comment to an aggregation', async () => {
const comment = 'Hello Parse';
const comment = `Hello Parse ${Date.now()}`;
const object = new TestObject();
object.set('name', 'object');
await object.save();
@@ -100,7 +148,23 @@ describe_only_db('mongo')('Parse.Query with comment testing', () => {
explain: true,
comment: comment,
});
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
// Wait for profile entry to appear with retry logic
let result;
const maxRetries = 10;
const retryDelay = 100;
for (let i = 0; i < maxRetries; i++) {
result = await database.collection('system.profile').findOne(
{ 'command.explain.comment': comment },
{ sort: { ts: -1 } }
);
if (result) {
break;
}
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
expect(result).toBeDefined();
expect(result.command.explain.comment).toBe(comment);
});
});

View File

@@ -517,7 +517,7 @@ describe('Parse.Relation testing', () => {
// Parent object is un-fetched, so this will call /1/classes/Car instead
// of /1/classes/Wheel and pass { "redirectClassNameForKey":"wheels" }.
return query.find(origWheel.id);
return query.find();
})
.then(function (results) {
// Make sure this is Wheel and not Car.

View File

@@ -7,6 +7,15 @@ const { SpecReporter } = require('jasmine-spec-reporter');
const SchemaCache = require('../lib/Adapters/Cache/SchemaCache').default;
const { sleep, Connections } = require('../lib/TestUtils');
const originalFetch = global.fetch;
let fetchWasMocked = false;
global.restoreFetch = () => {
global.fetch = originalFetch;
fetchWasMocked = false;
}
// Ensure localhost resolves to ipv4 address first on node v17+
if (dns.setDefaultResultOrder) {
dns.setDefaultResultOrder('ipv4first');
@@ -205,6 +214,7 @@ const reconfigureServer = async (changedConfiguration = {}) => {
};
beforeAll(async () => {
global.restoreFetch();
await reconfigureServer();
Parse.initialize('test', 'test', 'test');
Parse.serverURL = serverURL;
@@ -212,7 +222,18 @@ beforeAll(async () => {
Parse.CoreManager.set('REQUEST_ATTEMPT_LIMIT', 1);
});
beforeEach(async () => {
if(fetchWasMocked) {
global.restoreFetch();
}
});
global.afterEachFn = async () => {
// Restore fetch to prevent mock pollution between tests (only if it was mocked)
if (fetchWasMocked) {
global.restoreFetch();
}
Parse.Cloud._removeAllHooks();
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient();
defaults.protectedFields = { _User: { '*': ['email'] } };
@@ -251,6 +272,7 @@ global.afterEachFn = async () => {
afterEach(global.afterEachFn);
afterAll(() => {
global.restoreFetch();
global.displayTestStats();
});
@@ -388,9 +410,22 @@ function mockShortLivedAuth() {
}
function mockFetch(mockResponses) {
global.fetch = jasmine.createSpy('fetch').and.callFake((url, options = { }) => {
const spy = jasmine.createSpy('fetch');
fetchWasMocked = true; // Track that fetch was mocked for cleanup
global.fetch = (url, options = {}) => {
// Allow requests to the Parse Server to pass through WITHOUT recording in spy
// This prevents tests from failing when they check that fetch wasn't called
// but the Parse SDK makes internal requests to the Parse Server
if (typeof url === 'string' && url.includes(serverURL)) {
return originalFetch(url, options);
}
// Record non-Parse-Server calls in the spy
spy(url, options);
options.method ||= 'GET';
const mockResponse = mockResponses.find(
const mockResponse = mockResponses?.find(
(mock) => mock.url === url && mock.method === options.method
);
@@ -402,7 +437,11 @@ function mockFetch(mockResponses) {
ok: false,
statusText: 'Unknown URL or method',
});
});
};
// Expose spy methods for test assertions
global.fetch.calls = spy.calls;
global.fetch.and = spy.and;
}

View File

@@ -175,12 +175,10 @@ describe('Vulnerabilities', () => {
},
});
});
await expectAsync(new Parse.Object('TestObject').save()).toBeRejectedWith(
new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
'Prohibited keyword in request data: {"key":"constructor"}.'
)
);
// The new Parse SDK handles prototype pollution prevention in .set()
// so no error is thrown, but the object prototype should not be polluted
await new Parse.Object('TestObject').save();
expect(Object.prototype.dummy).toBeUndefined();
});
it('denies creating global config with polluted data', async () => {
@@ -270,12 +268,10 @@ describe('Vulnerabilities', () => {
res.json({ success: object });
});
await Parse.Hooks.createTrigger('TestObject', 'beforeSave', hookServerURL + '/BeforeSave');
await expectAsync(new Parse.Object('TestObject').save()).toBeRejectedWith(
new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
'Prohibited keyword in request data: {"key":"constructor"}.'
)
);
// The new Parse SDK handles prototype pollution prevention in .set()
// so no error is thrown, but the object prototype should not be polluted
await new Parse.Object('TestObject').save();
expect(Object.prototype.dummy).toBeUndefined();
await new Promise(resolve => server.close(resolve));
});