mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72af528d53 | ||
|
|
1da3123118 | ||
|
|
79bd170d53 | ||
|
|
a7358b1e0c | ||
|
|
cf030af7f5 | ||
|
|
d5a057d1a7 | ||
|
|
2c43ddb74e | ||
|
|
07870f59ee |
@@ -1,3 +1,31 @@
|
||||
## [8.6.10](https://github.com/parse-community/parse-server/compare/8.6.9...8.6.10) (2026-03-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* JWT audience validation bypass in Google, Apple, and Facebook authentication adapters ([GHSA-x6fw-778m-wr9v](https://github.com/parse-community/parse-server/security/advisories/GHSA-x6fw-778m-wr9v)) ([#10114](https://github.com/parse-community/parse-server/issues/10114)) ([1da3123](https://github.com/parse-community/parse-server/commit/1da312311827a7790ad97852e8672119d40d529a))
|
||||
|
||||
## [8.6.9](https://github.com/parse-community/parse-server/compare/8.6.8...8.6.9) (2026-03-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* File metadata endpoint bypasses `beforeFind` / `afterFind` trigger authorization ([GHSA-hwx8-q9cg-mqmc](https://github.com/parse-community/parse-server/security/advisories/GHSA-hwx8-q9cg-mqmc)) ([#10107](https://github.com/parse-community/parse-server/issues/10107)) ([a7358b1](https://github.com/parse-community/parse-server/commit/a7358b1e0c58ef4c6b5e0ade772bf673b5f78fd0))
|
||||
|
||||
## [8.6.8](https://github.com/parse-community/parse-server/compare/8.6.7...8.6.8) (2026-03-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `PagesRouter` path traversal allows reading files outside configured pages directory ([GHSA-hm3f-q6rw-m6wh](https://github.com/parse-community/parse-server/security/advisories/GHSA-hm3f-q6rw-m6wh)) ([#10105](https://github.com/parse-community/parse-server/issues/10105)) ([d5a057d](https://github.com/parse-community/parse-server/commit/d5a057d1a7cd5f6713d93afa3ad6f764f74b6ed2))
|
||||
|
||||
## [8.6.7](https://github.com/parse-community/parse-server/compare/8.6.6...8.6.7) (2026-03-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Malformed `$regex` query leaks database error details in API response (GHSA-9cp7-3q5w-j92g) ([#10102](https://github.com/parse-community/parse-server/issues/10102)) ([07870f5](https://github.com/parse-community/parse-server/commit/07870f59eec03f5c2a5fb1732cb28787ca3f8152))
|
||||
|
||||
## [8.6.6](https://github.com/parse-community/parse-server/compare/8.6.5...8.6.6) (2026-03-05)
|
||||
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "8.6.6",
|
||||
"version": "8.6.10",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "parse-server",
|
||||
"version": "8.6.6",
|
||||
"version": "8.6.10",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "8.6.6",
|
||||
"version": "8.6.10",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
|
||||
@@ -484,7 +484,7 @@ describe('google auth adapter', () => {
|
||||
|
||||
it('should throw error with missing id_token', async () => {
|
||||
try {
|
||||
await google.validateAuthData({}, {});
|
||||
await google.validateAuthData({}, { clientId: 'secret' });
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('id token is invalid for this user.');
|
||||
@@ -493,7 +493,7 @@ describe('google auth adapter', () => {
|
||||
|
||||
it('should not decode invalid id_token', async () => {
|
||||
try {
|
||||
await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
|
||||
await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, { clientId: 'secret' });
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('provided token does not decode as JWT');
|
||||
@@ -646,6 +646,15 @@ describe('google auth adapter', () => {
|
||||
expect(e.message).toBe('auth data is invalid for this user.');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw error when clientId is not configured', async () => {
|
||||
try {
|
||||
await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
|
||||
fail('should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Google auth is not configured.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('keycloak auth adapter', () => {
|
||||
@@ -1202,6 +1211,15 @@ describe('apple signin auth adapter', () => {
|
||||
expect(e.message).toBe('auth data is invalid for this user.');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw error when clientId is not configured', async () => {
|
||||
try {
|
||||
await apple.validateAuthData({ id: 'the_user_id', token: 'the_token' }, {});
|
||||
fail('should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Apple auth is not configured.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('phant auth adapter', () => {
|
||||
@@ -1237,19 +1255,9 @@ describe('facebook limited auth adapter', () => {
|
||||
const authUtils = require('../lib/Adapters/Auth/utils');
|
||||
|
||||
// TODO: figure out a way to run this test alongside facebook classic tests
|
||||
xit('(using client id as string) should throw error with missing id_token', async () => {
|
||||
xit('should throw error with missing id_token', async () => {
|
||||
try {
|
||||
await facebook.validateAuthData({}, { clientId: 'secret' });
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Facebook auth is not configured.');
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: figure out a way to run this test alongside facebook classic tests
|
||||
xit('(using client id as array) should throw error with missing id_token', async () => {
|
||||
try {
|
||||
await facebook.validateAuthData({}, { clientId: ['secret'] });
|
||||
await facebook.validateAuthData({}, { appIds: ['secret'] });
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Facebook auth is not configured.');
|
||||
@@ -1260,7 +1268,7 @@ describe('facebook limited auth adapter', () => {
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: 'secret' }
|
||||
{ appIds: ['secret'] }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
@@ -1277,7 +1285,7 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: 'secret' }
|
||||
{ appIds: ['secret'] }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
@@ -1302,7 +1310,7 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: 'secret' }
|
||||
{ appIds: ['secret'] }
|
||||
);
|
||||
expect(result).toEqual(fakeClaim);
|
||||
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
|
||||
@@ -1323,7 +1331,7 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: 'secret' }
|
||||
{ appIds: ['secret'] }
|
||||
);
|
||||
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(['RS256']);
|
||||
});
|
||||
@@ -1337,7 +1345,7 @@ describe('facebook limited auth adapter', () => {
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: 'secret' }
|
||||
{ appIds: ['secret'] }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
@@ -1345,19 +1353,7 @@ describe('facebook limited auth adapter', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('(using client id as array) should not verify invalid id_token', async () => {
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: ['secret'] }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('provided token does not decode as JWT');
|
||||
}
|
||||
});
|
||||
|
||||
it_id('4bcb1a1a-11f8-4e12-a3f6-73f7e25e355a')(it)('using client id as string) should verify id_token (facebook.com)', async () => {
|
||||
it_id('4bcb1a1a-11f8-4e12-a3f6-73f7e25e355a')(it)('should verify id_token (facebook.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'secret',
|
||||
@@ -1372,12 +1368,12 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: 'secret' }
|
||||
{ appIds: ['secret'] }
|
||||
);
|
||||
expect(result).toEqual(fakeClaim);
|
||||
});
|
||||
|
||||
it_id('c521a272-2ac2-4d8b-b5ed-ea250336d8b1')(it)('(using client id as array) should verify id_token (facebook.com)', async () => {
|
||||
it_id('e3f16404-18e9-4a87-a555-4710cfbdac67')(it)('(using multiple appIds) should verify id_token (facebook.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'secret',
|
||||
@@ -1392,32 +1388,12 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: ['secret'] }
|
||||
{ appIds: ['secret', 'secret 123'] }
|
||||
);
|
||||
expect(result).toEqual(fakeClaim);
|
||||
});
|
||||
|
||||
it_id('e3f16404-18e9-4a87-a555-4710cfbdac67')(it)('(using client id as array with multiple items) should verify id_token (facebook.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: ['secret', 'secret 123'] }
|
||||
);
|
||||
expect(result).toEqual(fakeClaim);
|
||||
});
|
||||
|
||||
it_id('549c33a1-3a6b-4732-8cf6-8f010ad4569c')(it)('(using client id as string) should throw error with with invalid jwt issuer (facebook.com)', async () => {
|
||||
it_id('549c33a1-3a6b-4732-8cf6-8f010ad4569c')(it)('should throw error with with invalid jwt issuer (facebook.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
@@ -1431,7 +1407,7 @@ describe('facebook limited auth adapter', () => {
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: 'secret' }
|
||||
{ appIds: ['secret'] }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
@@ -1443,87 +1419,14 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
|
||||
// and a private key
|
||||
xit('(using client id as array) should throw error with with invalid jwt issuer', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{
|
||||
id: 'INSERT ID HERE',
|
||||
token: 'INSERT FACEBOOK TOKEN HERE WITH INVALID JWT ISSUER',
|
||||
},
|
||||
{ clientId: ['INSERT CLIENT ID HERE'] }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('(using client id as string) with token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{
|
||||
id: 'INSERT ID HERE',
|
||||
token: 'INSERT FACEBOOK TOKEN HERE WITH INVALID JWT ISSUER',
|
||||
},
|
||||
{ clientId: 'INSERT CLIENT ID HERE' }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
|
||||
// and a private key
|
||||
xit('(using client id as string) should throw error with invalid jwt clientId', async () => {
|
||||
xit('should throw error with invalid jwt audience', async () => {
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{
|
||||
id: 'INSERT ID HERE',
|
||||
token: 'INSERT FACEBOOK TOKEN HERE',
|
||||
},
|
||||
{ clientId: 'secret' }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('jwt audience invalid. expected: secret');
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
|
||||
// and a private key
|
||||
xit('(using client id as array) should throw error with invalid jwt clientId', async () => {
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{
|
||||
id: 'INSERT ID HERE',
|
||||
token: 'INSERT FACEBOOK TOKEN HERE',
|
||||
},
|
||||
{ clientId: ['secret'] }
|
||||
{ appIds: ['secret'] }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
@@ -1540,7 +1443,7 @@ describe('facebook limited auth adapter', () => {
|
||||
id: 'invalid user',
|
||||
token: 'INSERT FACEBOOK TOKEN HERE',
|
||||
},
|
||||
{ clientId: 'INSERT CLIENT ID HERE' }
|
||||
{ appIds: ['INSERT APP ID HERE'] }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
@@ -1551,7 +1454,7 @@ describe('facebook limited auth adapter', () => {
|
||||
it_id('c194d902-e697-46c9-a303-82c2d914473c')(it)('should throw error with with invalid user id (facebook.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'invalid_client_id',
|
||||
aud: 'invalid_app_id',
|
||||
sub: 'a_different_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
@@ -1563,13 +1466,22 @@ describe('facebook limited auth adapter', () => {
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: 'secret' }
|
||||
{ appIds: ['secret'] }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('auth data is invalid for this user.');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw error when appIds is not configured for Limited Login', async () => {
|
||||
try {
|
||||
await facebook.validateAuthData({ id: 'the_user_id', token: 'the_token' }, {});
|
||||
fail('should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Facebook auth is not configured.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('OTP TOTP auth adatper', () => {
|
||||
|
||||
@@ -4473,6 +4473,21 @@ describe('Parse.File hooks', () => {
|
||||
});
|
||||
expect(response.headers['content-disposition']).toBe(`attachment;filename=${file._name}`);
|
||||
});
|
||||
|
||||
it('beforeFind blocks metadata endpoint', async () => {
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save({ useMasterKey: true });
|
||||
Parse.Cloud.beforeFind(Parse.File, () => {
|
||||
throw 'unauthorized';
|
||||
});
|
||||
await expectAsync(
|
||||
request({
|
||||
url: `http://localhost:8378/1/files/test/metadata/${file._name}`,
|
||||
}).catch(e => {
|
||||
throw new Parse.Error(e.data.code, e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(new Parse.Error(Parse.Error.SCRIPT_FAILED, 'unauthorized'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cloud Config hooks', () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const request = require('../lib/request');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const mustache = require('mustache');
|
||||
const Utils = require('../lib/Utils');
|
||||
@@ -957,6 +958,50 @@ describe('Pages Router', () => {
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.text).toBe('Not found.');
|
||||
});
|
||||
|
||||
it('rejects requesting file from sibling directory with prefix-colliding name via encoded path traversal', async () => {
|
||||
// Create a temporary pages directory and a sibling directory whose name
|
||||
// starts with the same prefix (e.g. "pages" vs "pages-secret"), which
|
||||
// would bypass a naive `startsWith` check without a path separator.
|
||||
const baseDir = path.join(__dirname, 'tmp-pages-exploit-test');
|
||||
const pagesDir = path.join(baseDir, 'pages');
|
||||
const siblingDir = path.join(baseDir, 'pages-secret');
|
||||
const marker = `SECRET_CONTENT_${Date.now()}`;
|
||||
|
||||
try {
|
||||
await fs.mkdir(pagesDir, { recursive: true });
|
||||
await fs.mkdir(siblingDir, { recursive: true });
|
||||
// Copy a required HTML file so the pages router initializes correctly
|
||||
const publicDir = path.resolve(__dirname, '../public');
|
||||
const htmlFile = await fs.readFile(
|
||||
path.join(publicDir, 'email_verification_link_invalid.html'),
|
||||
'utf-8'
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(pagesDir, 'email_verification_link_invalid.html'),
|
||||
htmlFile
|
||||
);
|
||||
// Write a secret file in the sibling directory
|
||||
await fs.writeFile(path.join(siblingDir, 'secret.txt'), marker);
|
||||
|
||||
config.pages.pagesPath = pagesDir;
|
||||
await reconfigureServer(config);
|
||||
|
||||
// Use URL-encoded path traversal: %2e%2e%2f = ../
|
||||
// This reaches the sibling "pages-secret" directory which shares
|
||||
// the "pages" prefix with the configured pagesPath directory name.
|
||||
const url = `${config.publicServerURL}/apps/%2e%2e%2fpages-secret%2fsecret.txt`;
|
||||
const response = await request({
|
||||
url: url,
|
||||
followRedirects: false,
|
||||
}).catch(e => e);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.text).not.toContain(marker);
|
||||
} finally {
|
||||
await fs.rm(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom route', () => {
|
||||
|
||||
+2
-2
@@ -659,12 +659,12 @@ describe('server', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should not fail when Google signin is introduced without the optional clientId', done => {
|
||||
it('should not fail when Google signin is introduced with clientId', done => {
|
||||
const jwt = require('jsonwebtoken');
|
||||
const authUtils = require('../lib/Adapters/Auth/utils');
|
||||
|
||||
reconfigureServer({
|
||||
auth: { google: {} },
|
||||
auth: { google: { clientId: 'secret' } },
|
||||
})
|
||||
.then(() => {
|
||||
const fakeClaim = {
|
||||
|
||||
@@ -478,6 +478,75 @@ describe('Vulnerabilities', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Malformed $regex information disclosure', () => {
|
||||
it('should not leak database error internals for invalid regex pattern in class query', async () => {
|
||||
const logger = require('../lib/logger').default;
|
||||
const loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
|
||||
const obj = new Parse.Object('TestObject');
|
||||
await obj.save({ field: 'value' });
|
||||
|
||||
try {
|
||||
await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/TestObject`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
qs: {
|
||||
where: JSON.stringify({ field: { $regex: '[abc' } }),
|
||||
},
|
||||
});
|
||||
fail('Request should have failed');
|
||||
} catch (e) {
|
||||
expect(e.data.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
|
||||
expect(e.data.error).toBe('An internal server error occurred');
|
||||
expect(typeof e.data.error).toBe('string');
|
||||
expect(JSON.stringify(e.data)).not.toContain('errmsg');
|
||||
expect(JSON.stringify(e.data)).not.toContain('codeName');
|
||||
expect(JSON.stringify(e.data)).not.toContain('errorResponse');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith(
|
||||
'Sanitized error:',
|
||||
jasmine.stringMatching(/[Rr]egular expression/i)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not leak database error internals for invalid regex pattern in role query', async () => {
|
||||
const logger = require('../lib/logger').default;
|
||||
const loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
|
||||
const role = new Parse.Role('testrole', new Parse.ACL());
|
||||
await role.save(null, { useMasterKey: true });
|
||||
try {
|
||||
await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/roles`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
qs: {
|
||||
where: JSON.stringify({ name: { $regex: '[abc' } }),
|
||||
},
|
||||
});
|
||||
fail('Request should have failed');
|
||||
} catch (e) {
|
||||
expect(e.data.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
|
||||
expect(e.data.error).toBe('An internal server error occurred');
|
||||
expect(typeof e.data.error).toBe('string');
|
||||
expect(JSON.stringify(e.data)).not.toContain('errmsg');
|
||||
expect(JSON.stringify(e.data)).not.toContain('codeName');
|
||||
expect(JSON.stringify(e.data)).not.toContain('errorResponse');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith(
|
||||
'Sanitized error:',
|
||||
jasmine.stringMatching(/[Rr]egular expression/i)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Postgres regex sanitizater', () => {
|
||||
it('sanitizes the regex correctly to prevent Injection', async () => {
|
||||
const user = new Parse.User();
|
||||
|
||||
@@ -73,6 +73,13 @@ const getAppleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
|
||||
};
|
||||
|
||||
const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => {
|
||||
if (!clientId) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Apple auth is not configured.'
|
||||
);
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
|
||||
}
|
||||
|
||||
@@ -52,8 +52,6 @@
|
||||
* - `>= 6.5.6 < 7`
|
||||
* - `>= 7.0.1`
|
||||
*
|
||||
* Secure authentication is recommended to ensure proper data protection and compliance with Facebook's guidelines.
|
||||
*
|
||||
* @see {@link https://developers.facebook.com/docs/facebook-login/limited-login/ Facebook Limited Login}
|
||||
* @see {@link https://developers.facebook.com/docs/facebook-login/facebook-login-for-business/ Facebook Login for Business}
|
||||
*/
|
||||
@@ -131,7 +129,14 @@ const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
|
||||
return key;
|
||||
};
|
||||
|
||||
const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => {
|
||||
const verifyIdToken = async ({ token, id }, { appIds, cacheMaxEntries, cacheMaxAge }) => {
|
||||
if (!Array.isArray(appIds) || !appIds.length) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Facebook auth is not configured.'
|
||||
);
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'id token is invalid for this user.');
|
||||
}
|
||||
@@ -150,7 +155,7 @@ const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMa
|
||||
jwtClaims = jwt.verify(token, signingKey, {
|
||||
algorithms: ['RS256'],
|
||||
// the audience can be checked against a string, a regular expression or a list of strings and/or regular expressions.
|
||||
audience: clientId,
|
||||
audience: appIds,
|
||||
});
|
||||
} catch (exception) {
|
||||
const message = exception.message;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* @class GoogleAdapter
|
||||
* @param {Object} options - The adapter configuration options.
|
||||
* @param {string} options.clientId - Your Google application Client ID. Required for authentication.
|
||||
* @param {string} options.clientId - Your Google application Client ID.
|
||||
* @param {number} [options.cacheMaxEntries] - Maximum number of JWKS cache entries. Default: 5.
|
||||
* @param {number} [options.cacheMaxAge] - Maximum age of JWKS cache entries in ms. Default: 3600000 (1 hour).
|
||||
*
|
||||
@@ -74,6 +74,13 @@ const getGoogleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
|
||||
};
|
||||
|
||||
async function verifyIdToken({ id_token: token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) {
|
||||
if (!clientId) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Google auth is not configured.'
|
||||
);
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
|
||||
}
|
||||
@@ -109,13 +116,6 @@ async function verifyIdToken({ id_token: token, id }, { clientId, cacheMaxEntrie
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `auth data is invalid for this user.`);
|
||||
}
|
||||
|
||||
if (clientId && jwtClaims.aud !== clientId) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
`id token not authorized for this clientId.`
|
||||
);
|
||||
}
|
||||
|
||||
return jwtClaims;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import SchemaCache from '../Adapters/Cache/SchemaCache';
|
||||
import type { LoadSchemaOptions } from './types';
|
||||
import type { ParseServerOptions } from '../Options';
|
||||
import type { QueryOptions, FullQueryOptions } from '../Adapters/Storage/StorageAdapter';
|
||||
import { createSanitizedError } from '../Error';
|
||||
|
||||
function addWriteACL(query, acl) {
|
||||
const newQuery = _.cloneDeep(query);
|
||||
@@ -1354,7 +1355,19 @@ class DatabaseController {
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, error);
|
||||
if (error instanceof Parse.Error) {
|
||||
throw error;
|
||||
}
|
||||
const detailedMessage =
|
||||
typeof error === 'string'
|
||||
? error
|
||||
: error?.message || 'An internal server error occurred';
|
||||
throw createSanitizedError(
|
||||
Parse.Error.INTERNAL_SERVER_ERROR,
|
||||
detailedMessage,
|
||||
this.options,
|
||||
'An internal server error occurred'
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+4
-2
@@ -6,9 +6,11 @@ import defaultLogger from './logger';
|
||||
*
|
||||
* @param {number} errorCode - The Parse.Error code (e.g., Parse.Error.OPERATION_FORBIDDEN)
|
||||
* @param {string} detailedMessage - The detailed error message to log server-side
|
||||
* @param {object} config - Parse Server config with enableSanitizedErrorResponse
|
||||
* @param {string} [sanitizedMessage='Permission denied'] - The sanitized message to return to clients
|
||||
* @returns {Parse.Error} A Parse.Error with sanitized message
|
||||
*/
|
||||
function createSanitizedError(errorCode, detailedMessage, config) {
|
||||
function createSanitizedError(errorCode, detailedMessage, config, sanitizedMessage = 'Permission denied') {
|
||||
// On testing we need to add a prefix to the message to allow to find the correct call in the TestUtils.js file
|
||||
if (process.env.TESTING) {
|
||||
defaultLogger.error('Sanitized error:', detailedMessage);
|
||||
@@ -16,7 +18,7 @@ function createSanitizedError(errorCode, detailedMessage, config) {
|
||||
defaultLogger.error(detailedMessage);
|
||||
}
|
||||
|
||||
return new Parse.Error(errorCode, config?.enableSanitizedErrorResponse !== false ? 'Permission denied' : detailedMessage);
|
||||
return new Parse.Error(errorCode, config?.enableSanitizedErrorResponse !== false ? sanitizedMessage : detailedMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -317,14 +317,45 @@ export class FilesRouter {
|
||||
async metadataHandler(req, res) {
|
||||
try {
|
||||
const config = Config.get(req.params.appId);
|
||||
if (!config) {
|
||||
res.status(200);
|
||||
res.json({});
|
||||
return;
|
||||
}
|
||||
const { filesController } = config;
|
||||
const { filename } = req.params;
|
||||
const data = await filesController.getMetadata(filename);
|
||||
let { filename } = req.params;
|
||||
const file = new Parse.File(filename, { base64: '' });
|
||||
const triggerResult = await triggers.maybeRunFileTrigger(
|
||||
triggers.Types.beforeFind,
|
||||
{ file },
|
||||
config,
|
||||
req.auth
|
||||
);
|
||||
if (triggerResult?.file?._name) {
|
||||
filename = triggerResult.file._name;
|
||||
}
|
||||
const data = await filesController.getMetadata(filename).catch(() => {
|
||||
res.status(200);
|
||||
res.json({});
|
||||
});
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
await triggers.maybeRunFileTrigger(
|
||||
triggers.Types.afterFind,
|
||||
{ file },
|
||||
config,
|
||||
req.auth
|
||||
);
|
||||
res.status(200);
|
||||
res.json(data);
|
||||
} catch {
|
||||
res.status(200);
|
||||
res.json({});
|
||||
} catch (e) {
|
||||
const err = triggers.resolveError(e, {
|
||||
code: Parse.Error.SCRIPT_FAILED,
|
||||
message: 'Could not get file metadata.',
|
||||
});
|
||||
res.status(403);
|
||||
res.json({ code: err.code, error: err.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,7 +500,7 @@ export class PagesRouter extends PromiseRouter {
|
||||
const normalizedPath = path.normalize(filePath);
|
||||
|
||||
// Abort if the path is outside of the path directory scope
|
||||
if (!normalizedPath.startsWith(this.pagesPath)) {
|
||||
if (!normalizedPath.startsWith(this.pagesPath + path.sep)) {
|
||||
throw errors.fileOutsideAllowedScope;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user