Compare commits

...
27 Commits
Author SHA1 Message Date
semantic-release-bot 62bb396ab4 chore(release): 5.5.4 [skip ci]
## [5.5.4](https://github.com/parse-community/parse-server/compare/5.5.3...5.5.4) (2023-07-30)

### Bug Fixes

* Security upgrade semver from 7.5.1 to 7.5.2 ([#8704](https://github.com/parse-community/parse-server/issues/8704)) ([c7fa3b9](https://github.com/parse-community/parse-server/commit/c7fa3b92a1d1289d0e281834175ed8807690eb9b))
2023-07-30 22:01:35 +00:00
m12331 c7fa3b92a1 fix: Security upgrade semver from 7.5.1 to 7.5.2 (#8704) 2023-07-30 23:54:12 +02:00
semantic-release-bot 9c0abe0120 chore(release): 5.5.3 [skip ci]
## [5.5.3](https://github.com/parse-community/parse-server/compare/5.5.2...5.5.3) (2023-06-29)

### Bug Fixes

* Server does not start via CLI when `auth` option is set ([#8669](https://github.com/parse-community/parse-server/issues/8669)) ([601da1e](https://github.com/parse-community/parse-server/commit/601da1ee3c8a22c48fa0209054a2e0f0474cf521))
2023-06-29 20:06:24 +00:00
Daniel 601da1ee3c fix: Server does not start via CLI when auth option is set (#8669) 2023-06-29 21:59:08 +02:00
semantic-release-bot e6374e791d chore(release): 5.5.2 [skip ci]
## [5.5.2](https://github.com/parse-community/parse-server/compare/5.5.1...5.5.2) (2023-06-28)

### Bug Fixes

* Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-462x-c3jw-7vr6](https://github.com/parse-community/parse-server/security/advisories/GHSA-462x-c3jw-7vr6) ([#8675](https://github.com/parse-community/parse-server/issues/8675)) ([5fad292](https://github.com/parse-community/parse-server/commit/5fad2928fb8ee17304abcdcf259932f827d8c81f))
2023-06-28 21:06:50 +00:00
Manuel 5fad2928fb fix: Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-462x-c3jw-7vr6](https://github.com/parse-community/parse-server/security/advisories/GHSA-462x-c3jw-7vr6) (#8675) 2023-06-28 22:59:09 +02:00
Parse Platform a0360712c8 refactor: Upgrade semver from 7.3.8 to 7.5.1 (#8606) 2023-06-04 18:05:20 +02:00
Parse Platform f5c6b3e8e5 refactor: Upgrade body-parser from 1.20.1 to 1.20.2 (#8607) 2023-06-04 00:05:34 +02:00
Parse Platform 733dc29e04 refactor: Upgrade winston from 3.8.1 to 3.8.2 (#8609) 2023-06-03 14:15:42 +02:00
Snyk bot e13f7bbb18 refactor: Upgrade express from 4.18.1 to 4.18.2 (#8600) 2023-06-01 15:56:52 +02:00
Snyk bot 81d51f3bde refactor: Upgrade ws from 8.9.0 to 8.13.0 (#8567) 2023-05-28 13:23:32 +02:00
semantic-release-bot c83b343aae chore(release): 5.5.1 [skip ci]
## [5.5.1](https://github.com/parse-community/parse-server/compare/5.5.0...5.5.1) (2023-05-23)

### Bug Fixes

* Security upgrade @parse/push-adapter from 4.1.2 to 4.1.3 ([#8571](https://github.com/parse-community/parse-server/issues/8571)) ([8e83cac](https://github.com/parse-community/parse-server/commit/8e83cac02d6258e9b84b69d4e76da7d771a5eac7))
2023-05-23 23:47:49 +00:00
Parse Platform 8e83cac02d fix: Security upgrade @parse/push-adapter from 4.1.2 to 4.1.3 (#8571) 2023-05-24 01:40:08 +02:00
Snyk bot d8bff571ae refactor: Upgrade @graphql-tools/merge from 8.3.17 to 8.4.1 (#8555) 2023-05-21 22:59:57 +02:00
Manuel c0a9ff89af ci: Fix outdated ubuntu version (#8540) 2023-05-21 01:52:59 +02:00
semantic-release-bot ac90cb8c35 chore(release): 5.5.0 [skip ci]
# [5.5.0](https://github.com/parse-community/parse-server/compare/5.4.3...5.5.0) (2023-05-20)

### Features

* Add new Parse Server option `fileUpload.fileExtensions` to restrict file upload by file extension; this fixes a security vulnerability in which a phishing attack could be performed using an uploaded HTML file; by default the new option only allows file extensions matching the regex pattern `^[^hH][^tT][^mM][^lL]?$`, which excludes HTML files; this fix is released as a patch version given the severity of this vulnerability, however, if your app currently depends on uploading files with HTML file extensions then this may be a breaking change and you could allow HTML file upload by setting the option to `['.*']` ([#8537](https://github.com/parse-community/parse-server/issues/8537)) ([196e05f](https://github.com/parse-community/parse-server/commit/196e05f047a65f0fa96910905bb5bf24b6d30338))
2023-05-20 23:22:06 +00:00
Manuel 196e05f047 feat: Add new Parse Server option fileUpload.fileExtensions to restrict file upload by file extension; this fixes a security vulnerability in which a phishing attack could be performed using an uploaded HTML file; by default the new option only allows file extensions matching the regex pattern ^[^hH][^tT][^mM][^lL]?$, which excludes HTML files; this fix is released as a patch version given the severity of this vulnerability, however, if your app currently depends on uploading files with HTML file extensions then this may be a breaking change and you could allow HTML file upload by setting the option to ['.*'] (#8537) 2023-05-21 01:14:27 +02:00
semantic-release-bot e9ae4351a1 chore(release): 5.4.3 [skip ci]
## [5.4.3](https://github.com/parse-community/parse-server/compare/5.4.2...5.4.3) (2023-03-22)

### Bug Fixes

* Unable to create new role if `beforeSave` hook exists ([#8474](https://github.com/parse-community/parse-server/issues/8474)) ([4f0f0ec](https://github.com/parse-community/parse-server/commit/4f0f0ec4bb7334adf64fcbfb80589727dc46906d))
2023-03-22 20:30:02 +00:00
yog27ray 4f0f0ec4bb fix: Unable to create new role if beforeSave hook exists (#8474) 2023-03-22 21:22:32 +01:00
Parse Platform 0ec923937b refactor: Upgrade @graphql-tools/merge from 8.3.6 to 8.3.17 (#8437) 2023-02-24 19:08:59 +01:00
semantic-release-bot b905137278 chore(release): 5.4.2 [skip ci]
## [5.4.2](https://github.com/parse-community/parse-server/compare/5.4.1...5.4.2) (2023-02-16)

### Bug Fixes

* Security upgrade jsonwebtoken to 9.0.0 ([#8431](https://github.com/parse-community/parse-server/issues/8431)) ([2c19c2e](https://github.com/parse-community/parse-server/commit/2c19c2e4d4cfdeefe31e0ec11d2dc0e8ee508a91))
2023-02-16 19:00:29 +00:00
Daniel 2c19c2e4d4 fix: Security upgrade jsonwebtoken to 9.0.0 (#8431) 2023-02-16 19:52:48 +01:00
semantic-release-bot 30576f1091 chore(release): 5.4.1 [skip ci]
## [5.4.1](https://github.com/parse-community/parse-server/compare/5.4.0...5.4.1) (2023-01-31)

### Bug Fixes

* The client IP address may be determined incorrectly in some cases; it is now required to set the Parse Server option `trustProxy` accordingly if Parse Server runs behind a proxy server, see the express framework's [trust proxy](https://expressjs.com/en/guide/behind-proxies.html) setting; this fixes a security vulnerability in which the Parse Server option `masterKeyIps` may be circumvented, see [GHSA-vm5r-c87r-pf6x](https://github.com/parse-community/parse-server/security/advisories/GHSA-vm5r-c87r-pf6x) ([#8369](https://github.com/parse-community/parse-server/issues/8369)) ([e016d81](https://github.com/parse-community/parse-server/commit/e016d813e083ce6828f9abce245d15b681a224d8))
2023-01-31 03:57:58 +00:00
Manuel e016d813e0 fix: The client IP address may be determined incorrectly in some cases; it is now required to set the Parse Server option trustProxy accordingly if Parse Server runs behind a proxy server, see the express framework's [trust proxy](https://expressjs.com/en/guide/behind-proxies.html) setting; this fixes a security vulnerability in which the Parse Server option masterKeyIps may be circumvented, see [GHSA-vm5r-c87r-pf6x](https://github.com/parse-community/parse-server/security/advisories/GHSA-vm5r-c87r-pf6x) (#8369) 2023-01-05 14:20:40 +01:00
Manuel c8bc200729 ci: Add LTS branches to CI workflow 2023-01-05 13:20:43 +01:00
Manuel Trezza 09d04b0aad ci: update auto-release workflow 2022-11-19 12:35:00 +01:00
Manuel Trezza 38f64be596 ci: update auto-release for LTS 2022-11-19 12:31:43 +01:00
30 changed files with 875 additions and 2376 deletions
+3
View File
@@ -24,5 +24,8 @@
"space-infix-ops": "error",
"no-useless-escape": "off",
"require-atomic-updates": "off"
},
"globals": {
"Parse": true
}
}
+9 -9
View File
@@ -1,9 +1,9 @@
name: ci
on:
push:
branches: [ release, alpha, beta ]
branches: [ release*, alpha, beta ]
pull_request:
branches: [ release, alpha, beta ]
branches: [ release*, alpha, beta ]
env:
NODE_VERSION: 18.1.0
PARSE_SERVER_TEST_TIMEOUT: 20000
@@ -32,7 +32,7 @@ jobs:
check-ci:
name: Node Engine Check
timeout-minutes: 15
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.NODE_VERSION }}
@@ -53,7 +53,7 @@ jobs:
check-lint:
name: Lint
timeout-minutes: 15
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.NODE_VERSION }}
@@ -73,7 +73,7 @@ jobs:
check-circular:
name: Circular Dependencies
timeout-minutes: 5
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.NODE_VERSION }}
@@ -93,7 +93,7 @@ jobs:
check-docker:
name: Docker Build
timeout-minutes: 15
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
@@ -110,7 +110,7 @@ jobs:
check-lock-file-version:
name: NPM Lock File Version
timeout-minutes: 5
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Check NPM lock file version
@@ -180,7 +180,7 @@ jobs:
fail-fast: false
name: ${{ matrix.name }}
timeout-minutes: 15
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
services:
redis:
image: redis
@@ -245,7 +245,7 @@ jobs:
fail-fast: false
name: ${{ matrix.name }}
timeout-minutes: 15
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
services:
redis:
image: redis
+3 -3
View File
@@ -1,7 +1,7 @@
name: release-automated
on:
push:
branches: [ release, alpha, beta, next-major ]
branches: [ release, alpha, beta, next-major, release-4.x.x, release-5.x.x ]
jobs:
release:
runs-on: ubuntu-latest
@@ -42,7 +42,7 @@ jobs:
env:
REGISTRY: docker.io
IMAGE_NAME: parseplatform/parse-server
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
@@ -86,7 +86,7 @@ jobs:
docs:
needs: release
if: needs.release.outputs.current_tag != '' && github.ref == 'refs/heads/release'
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v2
+1 -1
View File
@@ -14,7 +14,7 @@ env:
IMAGE_NAME: parseplatform/parse-server
jobs:
build:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
File diff suppressed because it is too large Load Diff
+351 -322
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "5.4.0",
"version": "5.5.4",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -19,25 +19,25 @@
],
"license": "BSD-3-Clause",
"dependencies": {
"@graphql-yoga/node": "2.6.0",
"@graphql-tools/utils": "8.12.0",
"@graphql-tools/merge": "8.3.6",
"@graphql-tools/merge": "8.4.1",
"@graphql-tools/schema": "9.0.4",
"@graphql-tools/utils": "8.12.0",
"@graphql-yoga/node": "2.6.0",
"@parse/fs-files-adapter": "1.2.2",
"@parse/push-adapter": "4.1.2",
"@parse/push-adapter": "4.1.3",
"bcryptjs": "2.4.3",
"body-parser": "1.20.1",
"body-parser": "1.20.2",
"commander": "5.1.0",
"cors": "2.8.5",
"deepcopy": "2.1.0",
"express": "4.18.1",
"express": "4.18.2",
"follow-redirects": "1.15.2",
"graphql": "16.6.0",
"graphql-list-fields": "2.0.2",
"graphql-tag": "2.12.6",
"graphql-relay": "0.10.0",
"graphql-tag": "2.12.6",
"intersect": "1.0.1",
"jsonwebtoken": "8.5.1",
"jsonwebtoken": "9.0.0",
"jwks-rsa": "2.1.4",
"ldapjs": "2.3.3",
"lodash": "4.17.21",
@@ -50,13 +50,13 @@
"pg-promise": "10.12.0",
"pluralize": "8.0.0",
"redis": "3.1.2",
"semver": "7.3.8",
"semver": "7.5.2",
"subscriptions-transport-ws": "0.11.0",
"tv4": "1.3.0",
"uuid": "9.0.0",
"winston": "3.8.1",
"winston": "3.8.2",
"winston-daily-rotate-file": "4.7.1",
"ws": "8.9.0"
"ws": "8.13.0"
},
"devDependencies": {
"graphql-tag": "2.12.6",
+2 -15
View File
@@ -28,7 +28,7 @@ async function config() {
console.log(`Running on branch: ${branch}`);
// Set changelog file
const changelogFile = `./changelogs/CHANGELOG_${branch}.md`;
const changelogFile = `./changelogs/CHANGELOG_release.md`;
console.log(`Changelog file output to: ${changelogFile}`);
// Load template file contents
@@ -41,10 +41,7 @@ async function config() {
{ name: 'beta', prerelease: true },
'next-major',
// Long-Term-Support branches
// { name: 'release-1', range: '1.x.x', channel: '1.x' },
// { name: 'release-2', range: '2.x.x', channel: '2.x' },
// { name: 'release-3', range: '3.x.x', channel: '3.x' },
// { name: 'release-4', range: '4.x.x', channel: '4.x' },
{ name: 'release-5.x.x', range: '5.x.x', channel: '5.x.x' },
],
dryRun: false,
debug: true,
@@ -88,16 +85,6 @@ async function config() {
labels: ['type:ci'],
releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
}],
// Back-merge module runs last because if it fails it should not impede the release process
[
"@saithodev/semantic-release-backmerge",
{
"branches": [
{ from: "beta", to: "alpha" },
{ from: "release", to: "beta" },
]
}
],
],
};
+28 -25
View File
@@ -596,6 +596,7 @@ describe('instagram auth adapter', () => {
describe('google auth adapter', () => {
const google = require('../lib/Adapters/Auth/google');
const jwt = require('jsonwebtoken');
const authUtils = require('../lib/Adapters/Auth/utils');
it('should throw error with missing id_token', async () => {
try {
@@ -618,7 +619,7 @@ describe('google auth adapter', () => {
// it('should throw error if public key used to encode token is not available', async () => {
// const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } };
// try {
// spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
// spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
// await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
// fail();
@@ -637,7 +638,7 @@ describe('google auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const result = await google.validateAuthData(
@@ -653,7 +654,7 @@ describe('google auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
try {
@@ -677,7 +678,7 @@ describe('google auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
try {
@@ -699,7 +700,7 @@ describe('google auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
try {
@@ -1402,6 +1403,7 @@ describe('apple signin auth adapter', () => {
const apple = require('../lib/Adapters/Auth/apple');
const jwt = require('jsonwebtoken');
const util = require('util');
const authUtils = require('../lib/Adapters/Auth/utils');
it('(using client id as string) should throw error with missing id_token', async () => {
try {
@@ -1436,7 +1438,7 @@ describe('apple signin auth adapter', () => {
it('should throw error if public key used to encode token is not available', async () => {
const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } };
try {
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
@@ -1458,7 +1460,7 @@ describe('apple signin auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
@@ -1475,7 +1477,7 @@ describe('apple signin auth adapter', () => {
it('should not verify invalid id_token', async () => {
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1512,7 +1514,7 @@ describe('apple signin auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1534,7 +1536,7 @@ describe('apple signin auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1556,7 +1558,7 @@ describe('apple signin auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1576,7 +1578,7 @@ describe('apple signin auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1604,7 +1606,7 @@ describe('apple signin auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1633,7 +1635,7 @@ describe('apple signin auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1705,7 +1707,7 @@ describe('apple signin auth adapter', () => {
sub: 'a_different_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1972,6 +1974,7 @@ describe('facebook limited auth adapter', () => {
const facebook = require('../lib/Adapters/Auth/facebook');
const jwt = require('jsonwebtoken');
const util = require('util');
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 () => {
@@ -2010,7 +2013,7 @@ describe('facebook limited auth adapter', () => {
header: { kid: '789', alg: 'RS256' },
};
try {
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
@@ -2034,7 +2037,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const fakeGetSigningKeyAsyncFunction = () => {
return {
@@ -2056,7 +2059,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
@@ -2098,7 +2101,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
@@ -2125,7 +2128,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
@@ -2152,7 +2155,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
@@ -2177,7 +2180,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
@@ -2210,7 +2213,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
@@ -2244,7 +2247,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
@@ -2330,7 +2333,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
+14
View File
@@ -294,4 +294,18 @@ describe('execution', () => {
done.fail(data.toString());
});
});
it('can start Parse Server with auth via CLI', done => {
childProcess = spawn(binPath, ['./spec/configs/CLIConfigAuth.json'], { env: process.env });
childProcess.stdout.on('data', data => {
data = data.toString();
if (data.includes('parse-server running on')) {
done();
}
});
childProcess.stderr.on('data', data => {
data = data.toString();
done.fail(data.toString());
});
});
});
+3 -118
View File
@@ -158,78 +158,6 @@ describe('middlewares', () => {
});
});
it('should not succeed if the connection.remoteAddress does not belong to masterKeyIps list', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1', 'ip2'],
});
fakeReq.connection = { remoteAddress: 'ip3' };
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should succeed if the connection.remoteAddress does belong to masterKeyIps list', done => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1', 'ip2'],
});
fakeReq.connection = { remoteAddress: 'ip1' };
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
it('should not succeed if the socket.remoteAddress does not belong to masterKeyIps list', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1', 'ip2'],
});
fakeReq.socket = { remoteAddress: 'ip3' };
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should succeed if the socket.remoteAddress does belong to masterKeyIps list', done => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1', 'ip2'],
});
fakeReq.socket = { remoteAddress: 'ip1' };
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
it('should not succeed if the connection.socket.remoteAddress does not belong to masterKeyIps list', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1', 'ip2'],
});
fakeReq.connection = { socket: { remoteAddress: 'ip3' } };
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should succeed if the connection.socket.remoteAddress does belong to masterKeyIps list', done => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1', 'ip2'],
});
fakeReq.connection = { socket: { remoteAddress: 'ip1' } };
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
it('should allow any ip to use masterKey if masterKeyIps is empty', done => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
@@ -243,52 +171,9 @@ describe('middlewares', () => {
});
});
it('should succeed if xff header does belong to masterKeyIps', done => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1'],
});
fakeReq.headers['x-parse-master-key'] = 'masterKey';
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
it('should succeed if xff header with one ip does belong to masterKeyIps', done => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1'],
});
fakeReq.headers['x-parse-master-key'] = 'masterKey';
fakeReq.headers['x-forwarded-for'] = 'ip1';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
it('should not succeed if xff header does not belong to masterKeyIps', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip4'],
});
fakeReq.headers['x-parse-master-key'] = 'masterKey';
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should not succeed if xff header is empty and masterKeyIps is set', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1'],
});
fakeReq.headers['x-parse-master-key'] = 'masterKey';
fakeReq.headers['x-forwarded-for'] = '';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
it('can set trust proxy', async () => {
const server = await reconfigureServer({ trustProxy: 1 });
expect(server.app.parent.settings['trust proxy']).toBe(1);
});
it('should properly expose the headers', () => {
+163 -25
View File
@@ -37,8 +37,14 @@ describe('Parse.File testing', () => {
});
});
it('works with _ContentType', done => {
request({
it('works with _ContentType', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
fileExtensions: ['*'],
},
});
let response = await request({
method: 'POST',
url: 'http://localhost:8378/1/files/file',
body: JSON.stringify({
@@ -47,21 +53,18 @@ describe('Parse.File testing', () => {
_ContentType: 'text/html',
base64: 'PGh0bWw+PC9odG1sPgo=',
}),
}).then(response => {
const b = response.data;
expect(b.name).toMatch(/_file.html/);
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
request({ url: b.url }).then(response => {
const body = response.text;
try {
expect(response.headers['content-type']).toMatch('^text/html');
expect(body).toEqual('<html></html>\n');
} catch (e) {
jfail(e);
}
done();
});
});
const b = response.data;
expect(b.name).toMatch(/_file.html/);
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
response = await request({ url: b.url });
const body = response.text;
try {
expect(response.headers['content-type']).toMatch('^text/html');
expect(body).toEqual('<html></html>\n');
} catch (e) {
jfail(e);
}
});
it('works without Content-Type', done => {
@@ -351,25 +354,29 @@ describe('Parse.File testing', () => {
ok(object.toJSON().file.url);
});
it('content-type used with no extension', done => {
it('content-type used with no extension', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
fileExtensions: ['*'],
},
});
const headers = {
'Content-Type': 'text/html',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
let response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file',
body: 'fee fi fo',
}).then(response => {
const b = response.data;
expect(b.name).toMatch(/\.html$/);
request({ url: b.url }).then(response => {
expect(response.headers['content-type']).toMatch(/^text\/html/);
done();
});
});
const b = response.data;
expect(b.name).toMatch(/\.html$/);
response = await request({ url: b.url });
expect(response.headers['content-type']).toMatch(/^text\/html/);
});
it('filename is url encoded', done => {
@@ -1298,6 +1305,137 @@ describe('Parse.File testing', () => {
await expectAsync(reconfigureServer({ fileUpload: { [key]: value } })).toBeResolved();
}
}
await expectAsync(
reconfigureServer({
fileUpload: {
fileExtensions: 1,
},
})
).toBeRejectedWith('fileUpload.fileExtensions must be an array.');
});
});
describe('fileExtensions', () => {
it('works with _ContentType', async () => {
await reconfigureServer({
silent: false,
fileUpload: {
enableForPublic: true,
fileExtensions: ['png'],
},
});
await expectAsync(
request({
method: 'POST',
url: 'http://localhost:8378/1/files/file',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'text/html',
base64: 'PGh0bWw+PC9odG1sPgo=',
}),
}).catch(e => {
throw new Error(e.data.error);
})
).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
);
});
it('works without Content-Type', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
},
});
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
await expectAsync(
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file.html',
body: '<html></html>\n',
}).catch(e => {
throw new Error(e.data.error);
})
).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
);
});
it('works with array', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
fileExtensions: ['jpg'],
},
});
await expectAsync(
request({
method: 'POST',
url: 'http://localhost:8378/1/files/file',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'text/html',
base64: 'PGh0bWw+PC9odG1sPgo=',
}),
}).catch(e => {
throw new Error(e.data.error);
})
).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
);
});
it('works with array without Content-Type', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
fileExtensions: ['jpg'],
},
});
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
await expectAsync(
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/file.html',
body: '<html></html>\n',
}).catch(e => {
throw new Error(e.data.error);
})
).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
);
});
it('works with array with correct file type', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
fileExtensions: ['html'],
},
});
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/files/file',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'text/html',
base64: 'PGh0bWw+PC9odG1sPgo=',
}),
});
const b = response.data;
expect(b.name).toMatch(/_file.html$/);
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
});
});
});
+8
View File
@@ -601,4 +601,12 @@ describe('Parse Role testing', () => {
});
});
});
it('should save role when beforeSave hook for _Role is present.', async done => {
Parse.Cloud.beforeSave('_Role', () => {});
const role = new Parse.Role('roleName', new Parse.ACL());
await role.save({}, { useMasterKey: true });
expect(role.id).toBeDefined();
done();
});
});
+11
View File
@@ -0,0 +1,11 @@
{
"appName": "test",
"appId": "test",
"masterKey": "test",
"logLevel": "error",
"auth": {
"facebook": {
"appIds": "test"
}
}
}
+2 -1
View File
@@ -553,6 +553,7 @@ describe('server', () => {
it('should not fail when Google signin is introduced without the optional clientId', done => {
const jwt = require('jsonwebtoken');
const authUtils = require('../lib/Adapters/Auth/utils');
reconfigureServer({
auth: { google: {} },
@@ -565,7 +566,7 @@ describe('server', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const user = new Parse.User();
user
+65
View File
@@ -138,6 +138,71 @@ describe('Vulnerabilities', () => {
);
});
it('denies creating global config with polluted data', async () => {
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
};
const params = {
method: 'PUT',
url: 'http://localhost:8378/1/config',
json: true,
body: {
params: {
welcomeMesssage: 'Welcome to Parse',
foo: { _bsontype: 'Code', code: 'shell' },
},
},
headers,
};
const response = await request(params).catch(e => e);
expect(response.status).toBe(400);
const text = JSON.parse(response.text);
expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME);
expect(text.error).toBe(
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
);
});
it('denies direct database write wih prohibited keys', async () => {
const Config = require('../lib/Config');
const config = Config.get(Parse.applicationId);
const user = {
objectId: '1234567890',
username: 'hello',
password: 'pass',
_session_token: 'abc',
foo: { _bsontype: 'Code', code: 'shell' },
};
await expectAsync(config.database.create('_User', user)).toBeRejectedWith(
new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
)
);
});
it('denies direct database update wih prohibited keys', async () => {
const Config = require('../lib/Config');
const config = Config.get(Parse.applicationId);
const user = {
objectId: '1234567890',
username: 'hello',
password: 'pass',
_session_token: 'abc',
foo: { _bsontype: 'Code', code: 'shell' },
};
await expectAsync(
config.database.update('_User', { _id: user.objectId }, user)
).toBeRejectedWith(
new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
)
);
});
it('denies creating a hook with polluted data', async () => {
const express = require('express');
const bodyParser = require('body-parser');
+2 -10
View File
@@ -5,6 +5,7 @@ const Parse = require('parse/node').Parse;
const jwksClient = require('jwks-rsa');
const util = require('util');
const jwt = require('jsonwebtoken');
const authUtils = require('./utils');
const TOKEN_ISSUER = 'https://appleid.apple.com';
@@ -30,21 +31,12 @@ const getAppleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
return key;
};
const getHeaderFromToken = token => {
const decodedToken = jwt.decode(token, { complete: true });
if (!decodedToken) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`);
}
return decodedToken.header;
};
const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => {
if (!token) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
}
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token);
const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
const ONE_HOUR_IN_MS = 3600000;
let jwtClaims;
+2 -10
View File
@@ -5,6 +5,7 @@ const jwksClient = require('jwks-rsa');
const util = require('util');
const jwt = require('jsonwebtoken');
const httpsRequest = require('./httpsRequest');
const authUtils = require('./utils');
const TOKEN_ISSUER = 'https://facebook.com';
@@ -73,21 +74,12 @@ const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
return key;
};
const getHeaderFromToken = token => {
const decodedToken = jwt.decode(token, { complete: true });
if (!decodedToken) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'provided token does not decode as JWT');
}
return decodedToken.header;
};
const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => {
if (!token) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'id token is invalid for this user.');
}
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token);
const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
const ONE_HOUR_IN_MS = 3600000;
let jwtClaims;
+2 -11
View File
@@ -5,6 +5,7 @@ var Parse = require('parse/node').Parse;
const https = require('https');
const jwt = require('jsonwebtoken');
const authUtils = require('./utils');
const TOKEN_ISSUER = 'accounts.google.com';
const HTTPS_TOKEN_ISSUER = 'https://accounts.google.com';
@@ -51,22 +52,12 @@ function getGoogleKeyByKeyId(keyId) {
});
}
function getHeaderFromToken(token) {
const decodedToken = jwt.decode(token, { complete: true });
if (!decodedToken) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`);
}
return decodedToken.header;
}
async function verifyIdToken({ id_token: token, id }, { clientId }) {
if (!token) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
}
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token);
const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
let jwtClaims;
const googleKey = await getGoogleKeyByKeyId(keyId);
+13
View File
@@ -0,0 +1,13 @@
const jwt = require('jsonwebtoken');
const Parse = require('parse/node').Parse;
const getHeaderFromToken = token => {
const decodedToken = jwt.decode(token, { complete: true });
if (!decodedToken) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`);
}
return decodedToken.header;
};
module.exports = {
getHeaderFromToken,
};
+5
View File
@@ -424,6 +424,11 @@ export class Config {
} else if (typeof fileUpload.enableForAuthenticatedUser !== 'boolean') {
throw 'fileUpload.enableForAuthenticatedUser must be a boolean value.';
}
if (fileUpload.fileExtensions === undefined) {
fileUpload.fileExtensions = FileUploadOptions.fileExtensions.default;
} else if (!Array.isArray(fileUpload.fileExtensions)) {
throw 'fileUpload.fileExtensions must be an array.';
}
}
static validateMasterKeyIps(masterKeyIps) {
+10
View File
@@ -467,6 +467,11 @@ class DatabaseController {
validateOnly: boolean = false,
validSchemaController: SchemaController.SchemaController
): Promise<any> {
try {
Utils.checkProhibitedKeywords(this.options, update);
} catch (error) {
return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
}
const originalQuery = query;
const originalUpdate = update;
// Make a copy of the object, so we don't mutate the incoming data.
@@ -797,6 +802,11 @@ class DatabaseController {
validateOnly: boolean = false,
validSchemaController: SchemaController.SchemaController
): Promise<any> {
try {
Utils.checkProhibitedKeywords(this.options, object);
} catch (error) {
return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
}
// Make a copy of the object, so we don't mutate the incoming data.
const originalObject = object;
object = transformObjectACL(object);
+14 -1
View File
@@ -95,7 +95,6 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_AUTH_PROVIDERS',
help:
'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication',
action: parsers.arrayParser,
},
cacheAdapter: {
env: 'PARSE_SERVER_CACHE_ADAPTER',
@@ -477,6 +476,13 @@ module.exports.ParseServerOptions = {
help: 'Starts the liveQuery server',
action: parsers.booleanParser,
},
trustProxy: {
env: 'PARSE_SERVER_TRUST_PROXY',
help:
'The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.',
action: parsers.objectParser,
default: [],
},
userSensitiveFields: {
env: 'PARSE_SERVER_USER_SENSITIVE_FIELDS',
help:
@@ -873,6 +879,13 @@ module.exports.FileUploadOptions = {
action: parsers.booleanParser,
default: false,
},
fileExtensions: {
env: 'PARSE_SERVER_FILE_UPLOAD_FILE_EXTENSIONS',
help:
"Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.<br><br>It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.<br><br>Defaults to `^[^hH][^tT][^mM][^lL]?$` which allows any file extension except HTML files.",
action: parsers.arrayParser,
default: ['^[^hH][^tT][^mM][^lL]?$'],
},
};
module.exports.DatabaseOptions = {
enableSchemaHooks: {
+3 -1
View File
@@ -19,7 +19,7 @@
* @property {Adapter<AnalyticsAdapter>} analyticsAdapter Adapter module for the analytics
* @property {String} appId Your Parse Application ID
* @property {String} appName Sets the app name
* @property {AuthAdapter[]} auth Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
* @property {Object} auth Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
* @property {Adapter<CacheAdapter>} cacheAdapter Adapter module for the cache
* @property {Number} cacheMaxSize Sets the maximum size for the in memory cache, defaults to 10000
* @property {Number} cacheTTL Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)
@@ -88,6 +88,7 @@
* @property {Number} sessionLength Session duration, in seconds, defaults to 1 year
* @property {Boolean} silent Disables console output
* @property {Boolean} startLiveQueryServer Starts the liveQuery server
* @property {Any} trustProxy The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.
* @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields
* @property {Boolean} verbose Set the logging to verbose
* @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process.<br><br>Default is `false`.
@@ -203,6 +204,7 @@
* @property {Boolean} enableForAnonymousUser Is true if file upload should be allowed for anonymous users.
* @property {Boolean} enableForAuthenticatedUser Is true if file upload should be allowed for authenticated users.
* @property {Boolean} enableForPublic Is true if file upload should be allowed for anyone, regardless of user authentication.
* @property {String[]} fileExtensions Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.<br><br>It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.<br><br>Defaults to `^[^hH][^tT][^mM][^lL]?$` which allows any file extension except HTML files.
*/
/**
+7 -1
View File
@@ -140,7 +140,7 @@ export interface ParseServerOptions {
allowCustomObjectId: ?boolean;
/* Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
:ENV: PARSE_SERVER_AUTH_PROVIDERS */
auth: ?(AuthAdapter[]);
auth: ?{ [string]: AuthAdapter };
/* Max file size for uploads, defaults to 20mb
:DEFAULT: 20mb */
maxUploadSize: ?string;
@@ -238,6 +238,9 @@ export interface ParseServerOptions {
cluster: ?NumberOrBoolean;
/* middleware for express server, can be string or function */
middleware: ?((() => void) | string);
/* The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.
:DEFAULT: false */
trustProxy: ?any;
/* Starts the liveQuery server */
startLiveQueryServer: ?boolean;
/* Live query server configuration options (will start the liveQuery server) */
@@ -493,6 +496,9 @@ export interface PasswordPolicyOptions {
}
export interface FileUploadOptions {
/* Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.<br><br>It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.<br><br>Defaults to `^[^hH][^tT][^mM][^lL]?$` which allows any file extension except HTML files.
:DEFAULT: ["^[^hH][^tT][^mM][^lL]?$"] */
fileExtensions: ?(string[]);
/* Is true if file upload should be allowed for anonymous users.
:DEFAULT: false */
enableForAnonymousUser: ?boolean;
+3
View File
@@ -304,6 +304,9 @@ class ParseServer {
options
);
}
if (options.trustProxy) {
app.set('trust proxy', options.trustProxy);
}
/* istanbul ignore next */
if (!process.env.TESTING) {
configureListeners(this);
+18 -26
View File
@@ -65,8 +65,6 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
}
}
this.checkProhibitedKeywords(data);
// When the operation is complete, this.response may have several
// fields.
// response: the actual data to be returned
@@ -87,7 +85,10 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
// Shared SchemaController to be reused to reduce the number of loadSchema() calls per request
// Once set the schemaData should be immutable
this.validSchemaController = null;
this.pendingOps = {};
this.pendingOps = {
operations: null,
identifier: null,
};
}
// A convenient method to perform all the steps of processing the
@@ -219,10 +220,13 @@ RestWrite.prototype.runBeforeSaveTrigger = function () {
}
const { originalObject, updatedObject } = this.buildParseObjects();
const identifier = updatedObject._getStateIdentifier();
const stateController = Parse.CoreManager.getObjectStateController();
const [pending] = stateController.getPendingOps(updatedObject._getStateIdentifier());
this.pendingOps = { ...pending };
const [pending] = stateController.getPendingOps(identifier);
this.pendingOps = {
operations: { ...pending },
identifier,
};
return Promise.resolve()
.then(() => {
@@ -282,7 +286,11 @@ RestWrite.prototype.runBeforeSaveTrigger = function () {
delete this.data.objectId;
}
}
this.checkProhibitedKeywords(this.data);
try {
Utils.checkProhibitedKeywords(this.config, this.data);
} catch (error) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, error);
}
});
};
@@ -1569,7 +1577,7 @@ RestWrite.prototype.runAfterSaveTrigger = function () {
.then(result => {
const jsonReturned = result && !result._toFullJSON;
if (jsonReturned) {
this.pendingOps = {};
this.pendingOps.operations = {};
this.response.response = result;
} else {
this.response.response = this._updateResponseWithData(
@@ -1673,10 +1681,9 @@ RestWrite.prototype.cleanUserAuthData = function () {
};
RestWrite.prototype._updateResponseWithData = function (response, data) {
const { updatedObject } = this.buildParseObjects();
const stateController = Parse.CoreManager.getObjectStateController();
const [pending] = stateController.getPendingOps(updatedObject._getStateIdentifier());
for (const key in this.pendingOps) {
const [pending] = stateController.getPendingOps(this.pendingOps.identifier);
for (const key in this.pendingOps.operations) {
if (!pending[key]) {
data[key] = this.originalData ? this.originalData[key] : { __op: 'Delete' };
this.storage.fieldsChangedByTrigger.push(key);
@@ -1725,20 +1732,5 @@ RestWrite.prototype._updateResponseWithData = function (response, data) {
return response;
};
RestWrite.prototype.checkProhibitedKeywords = function (data) {
if (this.config.requestKeywordDenylist) {
// Scan request data for denied keywords
for (const keyword of this.config.requestKeywordDenylist) {
const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
if (match) {
throw new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
);
}
}
}
};
export default RestWrite;
module.exports = RestWrite;
+38 -15
View File
@@ -138,25 +138,48 @@ export class FilesRouter {
return;
}
const fileExtensions = config.fileUpload?.fileExtensions;
if (!isMaster && fileExtensions) {
const isValidExtension = extension => {
return fileExtensions.some(ext => {
if (ext === '*') {
return true;
}
const regex = new RegExp(fileExtensions);
if (regex.test(extension)) {
return true;
}
});
};
let extension = contentType;
if (filename && filename.includes('.')) {
extension = filename.split('.')[1];
} else if (contentType && contentType.includes('/')) {
extension = contentType.split('/')[1];
}
extension = extension.split(' ').join('');
if (!isValidExtension(extension)) {
next(
new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
`File upload of extension ${extension} is disabled.`
)
);
return;
}
}
const base64 = req.body.toString('base64');
const file = new Parse.File(filename, { base64 }, contentType);
const { metadata = {}, tags = {} } = req.fileData || {};
if (req.config && req.config.requestKeywordDenylist) {
try {
// Scan request data for denied keywords
for (const keyword of req.config.requestKeywordDenylist) {
const match =
Utils.objectContainsKeyValue(metadata, keyword.key, keyword.value) ||
Utils.objectContainsKeyValue(tags, keyword.key, keyword.value);
if (match) {
next(
new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
)
);
return;
}
}
Utils.checkProhibitedKeywords(config, metadata);
Utils.checkProhibitedKeywords(config, tags);
} catch (error) {
next(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
return;
}
file.setTags(tags);
file.setMetadata(metadata);
+12
View File
@@ -358,6 +358,18 @@ class Utils {
}
return false;
}
static checkProhibitedKeywords(config, data) {
if (config?.requestKeywordDenylist) {
// Scan request data for denied keywords
for (const keyword of config.requestKeywordDenylist) {
const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
if (match) {
throw `Prohibited keyword in request data: ${JSON.stringify(keyword)}.`;
}
}
}
}
}
module.exports = Utils;
+1 -16
View File
@@ -280,22 +280,7 @@ export function handleParseHeaders(req, res, next) {
}
function getClientIp(req) {
if (req.headers['x-forwarded-for']) {
// try to get from x-forwared-for if it set (behind reverse proxy)
return req.headers['x-forwarded-for'].split(',')[0];
} else if (req.connection && req.connection.remoteAddress) {
// no proxy, try getting from connection.remoteAddress
return req.connection.remoteAddress;
} else if (req.socket) {
// try to get it from req.socket
return req.socket.remoteAddress;
} else if (req.connection && req.connection.socket) {
// try to get it form the connection.socket
return req.connection.socket.remoteAddress;
} else {
// if non above, fallback.
return req.ip;
}
return req.ip;
}
function httpAuth(req) {