mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c71ae7d478 | ||
|
|
6ae5835b19 | ||
|
|
0bfa6b7cc1 | ||
|
|
0be0b87b2d | ||
|
|
f3133acf21 | ||
|
|
7e1da90bb2 | ||
|
|
0e3cae5c67 | ||
|
|
f0d5232443 | ||
|
|
a3483d82c8 | ||
|
|
3c42584f59 | ||
|
|
97b1dcad3b | ||
|
|
3a5c38d2e4 | ||
|
|
93d643e3eb | ||
|
|
1306da7454 |
+45
-4
@@ -1,9 +1,50 @@
|
||||
## Parse Server Changelog
|
||||
# Parse Server Changelog
|
||||
|
||||
### master
|
||||
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.5.0...master)
|
||||
# 4.10.3
|
||||
|
||||
### 4.5.0
|
||||
## Security Fixes
|
||||
- Validate `explain` query parameter to avoid a server crash due to MongoDB bug [NODE-3463](https://jira.mongodb.org/browse/NODE-3463) (Kartal Kaan Bozdogan) [GHSA-xqp8-w826-hh6x](https://github.com/parse-community/parse-server/security/advisories/GHSA-xqp8-w826-hh6x)
|
||||
|
||||
# 4.10.2
|
||||
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.10.1...4.10.2)
|
||||
|
||||
## Fixes
|
||||
- Move graphql-tag from devDependencies to dependencies (Antonio Davi Macedo Coelho de Castro) [#7183](https://github.com/parse-community/parse-server/pull/7183)
|
||||
|
||||
# 4.10.1
|
||||
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.10.0...4.10.1)
|
||||
|
||||
## Security Fixes
|
||||
- Updated to Parse JS SDK 3.3.0 and other security fixes (Manuel Trezza) [#7508](https://github.com/parse-community/parse-server/pull/7508)
|
||||
|
||||
> ⚠️ This includes a security fix of the Parse JS SDK where `logIn` will default to `POST` instead of `GET` method. This may require changes in your deployment before you upgrade to this release, see the Parse JS SDK 3.0.0 [release notes](https://github.com/parse-community/Parse-SDK-JS/releases/tag/3.0.0).
|
||||
|
||||
# 4.10.0
|
||||
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.5.2...4.10.0)
|
||||
|
||||
*Versions >4.5.2 and <4.10.0 are skipped.*
|
||||
|
||||
> ⚠️ A security incident caused a number of incorrect version tags to be pushed to the Parse Server repository. These version tags linked to a personal fork of a contributor who had write access to the repository. The code to which these tags linked has not been reviewed or approved by Parse Platform. Even though no releases were published with these incorrect versions, it was possible to define a Parse Server dependency that pointed to these version tags, for example if you defined this dependency:
|
||||
> ```js
|
||||
> "parse-server": "git@github.com:parse-community/parse-server.git#4.9.3"
|
||||
> ```
|
||||
>
|
||||
> We have since deleted the incorrect version tags, but they may still show up if your personal fork on GitHub or locally. We do not know when these tags have been pushed to the Parse Server repository, but we first became aware of this issue on July 21, 2021. We are not aware of any malicious code or concerns related to privacy, security or legality (e.g. proprietary code). However, it has been reported that some functionality does not work as expected and the introduction of security vulnerabilities cannot be ruled out.
|
||||
>
|
||||
> You may be also affected if you used the Bitnami image for Parse Server. Bitnami picked up the incorrect version tag `4.9.3` and published a new Bitnami image for Parse Server.
|
||||
>
|
||||
>**If you are using any of the affected versions, we urgently recommend to upgrade to version `4.10.0`.**
|
||||
|
||||
# 4.5.2
|
||||
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.5.0...4.5.2)
|
||||
|
||||
## Security Fixes
|
||||
- SECURITY FIX: Fixes incorrect session property `authProvider: password` of anonymous users. When signing up an anonymous user, the session field `createdWith` indicates incorrectly that the session has been created using username and password with `authProvider: password`, instead of an anonymous sign-up with `authProvider: anonymous`. This fixes the issue by setting the correct `authProvider: anonymous` for future sign-ups of anonymous users. This fix does not fix incorrect `authProvider: password` for existing sessions of anonymous users. Consider this if your app logic depends on the `authProvider` field. (Corey Baker) [GHSA-23r4-5mxp-c7g5](https://github.com/parse-community/parse-server/security/advisories/GHSA-23r4-5mxp-c7g5)
|
||||
|
||||
# 4.5.1
|
||||
*This version was published by mistake and was deprecated.*
|
||||
|
||||
# 4.5.0
|
||||
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.4.0...4.5.0)
|
||||
|
||||
__BREAKING CHANGES:__
|
||||
|
||||
Generated
+735
-644
File diff suppressed because it is too large
Load Diff
+13
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "4.5.0",
|
||||
"version": "4.10.3",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -24,8 +24,8 @@
|
||||
"@graphql-tools/stitch": "6.2.4",
|
||||
"@graphql-tools/utils": "6.2.4",
|
||||
"@parse/fs-files-adapter": "1.2.0",
|
||||
"@parse/push-adapter": "3.4.0",
|
||||
"@parse/s3-files-adapter": "1.6.0",
|
||||
"@parse/push-adapter": "3.4.1",
|
||||
"@parse/s3-files-adapter": "1.6.2",
|
||||
"@parse/simple-mailgun-adapter": "1.1.0",
|
||||
"apollo-server-express": "2.19.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
@@ -38,29 +38,30 @@
|
||||
"graphql": "15.4.0",
|
||||
"graphql-list-fields": "2.0.2",
|
||||
"graphql-relay": "0.6.0",
|
||||
"graphql-tag": "2.12.5",
|
||||
"graphql-upload": "11.0.0",
|
||||
"intersect": "1.0.1",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"jwks-rsa": "1.11.0",
|
||||
"jwks-rsa": "1.12.3",
|
||||
"ldapjs": "2.2.2",
|
||||
"lodash": "4.17.20",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "5.1.1",
|
||||
"mime": "2.4.6",
|
||||
"mongodb": "3.6.3",
|
||||
"parse": "2.19.0",
|
||||
"mongodb": "3.6.11",
|
||||
"parse": "3.3.0",
|
||||
"pg-promise": "10.8.1",
|
||||
"pluralize": "8.0.0",
|
||||
"redis": "3.0.2",
|
||||
"redis": "3.1.2",
|
||||
"semver": "7.3.2",
|
||||
"subscriptions-transport-ws": "0.9.18",
|
||||
"subscriptions-transport-ws": "0.10.0",
|
||||
"tv4": "1.3.0",
|
||||
"uuid": "8.3.1",
|
||||
"winston": "3.3.3",
|
||||
"winston-daily-rotate-file": "4.5.0",
|
||||
"ws": "7.4.0"
|
||||
"ws": "7.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.10.0",
|
||||
"@babel/cli": "7.14.8",
|
||||
"@babel/core": "7.10.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.10.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.9.0",
|
||||
@@ -81,10 +82,9 @@
|
||||
"eslint-plugin-flowtype": "5.1.3",
|
||||
"flow-bin": "0.119.1",
|
||||
"form-data": "3.0.0",
|
||||
"graphql-tag": "2.10.1",
|
||||
"husky": "4.2.5",
|
||||
"jasmine": "3.5.0",
|
||||
"jsdoc": "3.6.3",
|
||||
"jsdoc": "3.6.7",
|
||||
"jsdoc-babel": "0.5.0",
|
||||
"lint-staged": "10.2.3",
|
||||
"mongodb-runner": "mongodb-js/runner",
|
||||
|
||||
+15
-7
@@ -87,13 +87,17 @@ describe('OAuth', function () {
|
||||
done();
|
||||
}
|
||||
|
||||
it('Should fail a GET request', done => {
|
||||
it('GET request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
/*
|
||||
This endpoint has been chosen to make a request to an endpoint that requires OAuth which fails due to missing authentication.
|
||||
Any other endpoint from the Twitter API that requires OAuth can be used instead in case the currently used endpoint deprecates.
|
||||
*/
|
||||
const options = {
|
||||
host: 'api.twitter.com',
|
||||
consumer_key: 'XXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
consumer_secret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
consumer_key: 'invalid_consumer_key',
|
||||
consumer_secret: 'invalid_consumer_secret',
|
||||
};
|
||||
const path = '/1.1/help/configuration.json';
|
||||
const path = '/1.1/favorites/list.json';
|
||||
const params = { lang: 'en' };
|
||||
const oauthClient = new OAuth(options);
|
||||
oauthClient.get(path, params).then(function (data) {
|
||||
@@ -101,11 +105,15 @@ describe('OAuth', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail a POST request', done => {
|
||||
it('POST request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
/*
|
||||
This endpoint has been chosen to make a request to an endpoint that requires OAuth which fails due to missing authentication.
|
||||
Any other endpoint from the Twitter API that requires OAuth can be used instead in case the currently used endpoint deprecates.
|
||||
*/
|
||||
const options = {
|
||||
host: 'api.twitter.com',
|
||||
consumer_key: 'XXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
consumer_secret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
consumer_key: 'invalid_consumer_key',
|
||||
consumer_secret: 'invalid_consumer_secret',
|
||||
};
|
||||
const body = {
|
||||
lang: 'en',
|
||||
|
||||
@@ -33,6 +33,54 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_only_db('mongo')('gracefully handles invalid explain values', async () => {
|
||||
// Note that anything that is not truthy (like 0) does not cause an exception, as they get swallowed up by ClassesRouter::optionsFromBody
|
||||
const values = [1, 'yolo', { a: 1 }, [1, 2, 3]];
|
||||
for (const value of values) {
|
||||
try {
|
||||
await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User?explain=${value}`,
|
||||
json: true,
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
fail('request did not throw');
|
||||
} catch (e) {
|
||||
// Expect that Parse Server did not crash
|
||||
expect(e.code).not.toEqual('ECONNRESET');
|
||||
// Expect that Parse Server validates the explain value and does not crash;
|
||||
// see https://jira.mongodb.org/browse/NODE-3463
|
||||
equal(e.data.code, Parse.Error.INVALID_QUERY);
|
||||
equal(e.data.error, 'Invalid value for explain');
|
||||
}
|
||||
// get queries (of the form '/classes/:className/:objectId' cannot have the explain key, see ClassesRouter.js)
|
||||
// so it is enough that we test find queries
|
||||
}
|
||||
});
|
||||
|
||||
it_only_db('mongo')('supports valid explain values', async () => {
|
||||
const values = [
|
||||
false,
|
||||
true,
|
||||
'queryPlanner',
|
||||
'executionStats',
|
||||
'allPlansExecution',
|
||||
// 'queryPlannerExtended' is excluded as it only applies to MongoDB Data Lake which is currently not available in our CI environment
|
||||
];
|
||||
for (const value of values) {
|
||||
const response = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User?explain=${value}`,
|
||||
json: true,
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
if (value) {
|
||||
expect(response.data.results.ok).toBe(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('searching for null', function (done) {
|
||||
const baz = new TestObject({ foo: null });
|
||||
const qux = new TestObject({ foo: 'qux' });
|
||||
|
||||
+56
-52
@@ -791,7 +791,7 @@ describe('Parse.User testing', () => {
|
||||
|
||||
user.set('username', 'test');
|
||||
await user.save();
|
||||
equal(Object.keys(user.attributes).length, 6);
|
||||
equal(Object.keys(user.attributes).length, 5);
|
||||
ok(user.attributes['username']);
|
||||
ok(user.attributes['email']);
|
||||
await user.destroy();
|
||||
@@ -2374,59 +2374,63 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('user get session from token on signup', done => {
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
return Parse.User.signUp('finn', 'human', { foo: 'bar' });
|
||||
})
|
||||
.then(user => {
|
||||
request({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Session-Token': user.getSessionToken(),
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
url: 'http://localhost:8378/1/sessions/me',
|
||||
}).then(response => {
|
||||
const b = response.data;
|
||||
expect(typeof b.sessionToken).toEqual('string');
|
||||
expect(typeof b.createdWith).toEqual('object');
|
||||
expect(b.createdWith.action).toEqual('signup');
|
||||
expect(typeof b.user).toEqual('object');
|
||||
expect(b.user.objectId).toEqual(user.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('user get session from token on signup', async () => {
|
||||
const user = await Parse.User.signUp('finn', 'human', { foo: 'bar' });
|
||||
const response = await request({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Session-Token': user.getSessionToken(),
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
url: 'http://localhost:8378/1/sessions/me',
|
||||
});
|
||||
const data = response.data;
|
||||
expect(typeof data.sessionToken).toEqual('string');
|
||||
expect(typeof data.createdWith).toEqual('object');
|
||||
expect(data.createdWith.action).toEqual('signup');
|
||||
expect(data.createdWith.authProvider).toEqual('password');
|
||||
expect(typeof data.user).toEqual('object');
|
||||
expect(data.user.objectId).toEqual(user.id);
|
||||
});
|
||||
|
||||
it('user get session from token on login', done => {
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
return Parse.User.signUp('finn', 'human', { foo: 'bar' });
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.User.logOut().then(() => {
|
||||
return Parse.User.logIn('finn', 'human');
|
||||
});
|
||||
})
|
||||
.then(user => {
|
||||
request({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Session-Token': user.getSessionToken(),
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
url: 'http://localhost:8378/1/sessions/me',
|
||||
}).then(response => {
|
||||
const b = response.data;
|
||||
expect(typeof b.sessionToken).toEqual('string');
|
||||
expect(typeof b.createdWith).toEqual('object');
|
||||
expect(b.createdWith.action).toEqual('login');
|
||||
expect(typeof b.user).toEqual('object');
|
||||
expect(b.user.objectId).toEqual(user.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('user get session from token on username/password login', async () => {
|
||||
await Parse.User.signUp('finn', 'human', { foo: 'bar' });
|
||||
await Parse.User.logOut();
|
||||
const user = await Parse.User.logIn('finn', 'human');
|
||||
const response = await request({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Session-Token': user.getSessionToken(),
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
url: 'http://localhost:8378/1/sessions/me',
|
||||
});
|
||||
const data = response.data;
|
||||
expect(typeof data.sessionToken).toEqual('string');
|
||||
expect(typeof data.createdWith).toEqual('object');
|
||||
expect(data.createdWith.action).toEqual('login');
|
||||
expect(data.createdWith.authProvider).toEqual('password');
|
||||
expect(typeof data.user).toEqual('object');
|
||||
expect(data.user.objectId).toEqual(user.id);
|
||||
});
|
||||
|
||||
it('user get session from token on anonymous login', async () => {
|
||||
const user = await Parse.AnonymousUtils.logIn();
|
||||
const response = await request({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Session-Token': user.getSessionToken(),
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
url: 'http://localhost:8378/1/sessions/me',
|
||||
});
|
||||
const data = response.data;
|
||||
expect(typeof data.sessionToken).toEqual('string');
|
||||
expect(typeof data.createdWith).toEqual('object');
|
||||
expect(data.createdWith.action).toEqual('login');
|
||||
expect(data.createdWith.authProvider).toEqual('anonymous');
|
||||
expect(typeof data.user).toEqual('object');
|
||||
expect(data.user.objectId).toEqual(user.id);
|
||||
});
|
||||
|
||||
it('user update session with other field', done => {
|
||||
|
||||
@@ -108,6 +108,23 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (
|
||||
return mongoObject;
|
||||
};
|
||||
|
||||
function validateExplainValue(explain) {
|
||||
if (explain) {
|
||||
// The list of allowed explain values is from node-mongodb-native/lib/explain.js
|
||||
const explainAllowedValues = [
|
||||
'queryPlanner',
|
||||
'queryPlannerExtended',
|
||||
'executionStats',
|
||||
'allPlansExecution',
|
||||
false,
|
||||
true,
|
||||
];
|
||||
if (!explainAllowedValues.includes(explain)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Invalid value for explain');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MongoStorageAdapter implements StorageAdapter {
|
||||
// Private
|
||||
_uri: string;
|
||||
@@ -562,6 +579,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
query: QueryType,
|
||||
{ skip, limit, sort, keys, readPreference, hint, caseInsensitive, explain }: QueryOptions
|
||||
): Promise<any> {
|
||||
validateExplainValue(explain);
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoWhere = transformWhere(className, query, schema);
|
||||
const mongoSort = _.mapKeys(sort, (value, fieldName) =>
|
||||
@@ -740,6 +758,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
hint: ?mixed,
|
||||
explain?: boolean
|
||||
) {
|
||||
validateExplainValue(explain);
|
||||
let isPointerField = false;
|
||||
pipeline = pipeline.map(stage => {
|
||||
if (stage.$group) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { loadAdapter } from '../Adapters/AdapterLoader';
|
||||
import { WSAdapter } from '../Adapters/WebSocketServer/WSAdapter';
|
||||
import logger from '../logger';
|
||||
import events from 'events';
|
||||
import { inspect } from 'util';
|
||||
|
||||
export class ParseWebSocketServer {
|
||||
server: Object;
|
||||
@@ -15,7 +16,7 @@ export class ParseWebSocketServer {
|
||||
wss.onConnection = ws => {
|
||||
ws.on('error', error => {
|
||||
logger.error(error.message);
|
||||
logger.error(JSON.stringify(ws));
|
||||
logger.error(inspect(ws, false));
|
||||
});
|
||||
onConnect(new ParseWebSocket(ws));
|
||||
// Send ping to client periodically
|
||||
|
||||
+1
-1
@@ -633,7 +633,7 @@ RestQuery.prototype.runFind = function (options = {}) {
|
||||
return this.config.database
|
||||
.find(this.className, this.restWhere, findOptions, this.auth)
|
||||
.then(results => {
|
||||
if (this.className === '_User' && findOptions.explain !== true) {
|
||||
if (this.className === '_User' && !findOptions.explain) {
|
||||
for (var result of results) {
|
||||
cleanResultAuthData(result);
|
||||
}
|
||||
|
||||
@@ -857,6 +857,10 @@ RestWrite.prototype.createSessionToken = async function () {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.storage['authProvider'] == null && this.data.authData) {
|
||||
this.storage['authProvider'] = Object.keys(this.data.authData).join(',');
|
||||
}
|
||||
|
||||
const { sessionData, createSession } = Auth.createSession(this.config, {
|
||||
userId: this.objectId(),
|
||||
createdWith: {
|
||||
|
||||
Reference in New Issue
Block a user