Compare commits

...
43 Commits
Author SHA1 Message Date
semantic-release-bot ad680bd312 chore(release): 4.10.11 [skip ci]
## [4.10.11](https://github.com/parse-community/parse-server/compare/4.10.10...4.10.11) (2022-06-17)

### Bug Fixes

* certificate in Apple Game Center auth adapter not validated; this fixes a security vulnerability in which authentication could be bypassed using a fake certificate; if you are using the Apple Gamer Center auth adapter it is your responsibility to keep its root certificate up-to-date and we advice you read the security advisory ([GHSA-rh9j-f5f8-rvgc](https://github.com/parse-community/parse-server/security/advisories/GHSA-rh9j-f5f8-rvgc)) ([145838d](https://github.com/parse-community/parse-server/commit/145838d2d9c1ecf76412a962a4ef61c712bcb0a7))
2022-06-17 16:38:16 +00:00
Manuel 145838d2d9 fix: certificate in Apple Game Center auth adapter not validated; this fixes a security vulnerability in which authentication could be bypassed using a fake certificate; if you are using the Apple Gamer Center auth adapter it is your responsibility to keep its root certificate up-to-date and we advice you read the security advisory ([GHSA-rh9j-f5f8-rvgc](https://github.com/parse-community/parse-server/security/advisories/GHSA-rh9j-f5f8-rvgc)) 2022-06-17 18:24:13 +02:00
Manuel Trezza 8580a524eb fix CI timeout 2022-06-17 17:57:22 +02:00
Manuel Trezza 53afafa13f Update gcenter.js 2022-06-17 17:30:54 +02:00
Manuel Trezza c411c48d49 Create game_center.pem 2022-06-17 16:19:25 +02:00
Manuel Trezza 07786c1666 fix adapter 2022-06-17 16:19:25 +02:00
semantic-release-bot b00b0410e6 chore(release): 4.10.10 [skip ci]
## [4.10.10](https://github.com/parse-community/parse-server/compare/4.10.9...4.10.10) (2022-05-01)

### Bug Fixes

* authentication bypass and denial of service (DoS) vulnerabilities in Apple Game Center auth adapter (GHSA-qf8x-vqjv-92gr) ([#7963](https://github.com/parse-community/parse-server/issues/7963)) ([1930a64](https://github.com/parse-community/parse-server/commit/1930a64e9c7e0d287b4ad9e57c6ee03b11b9cbc4))
2022-05-01 00:48:05 +00:00
Manuel 1930a64e9c fix: authentication bypass and denial of service (DoS) vulnerabilities in Apple Game Center auth adapter (GHSA-qf8x-vqjv-92gr) (#7963) 2022-05-01 02:46:57 +02:00
semantic-release-bot cd354b77a6 chore(release): 4.10.9 [skip ci]
## [4.10.9](https://github.com/parse-community/parse-server/compare/4.10.8...4.10.9) (2022-03-28)

### Bug Fixes

* security upgrade @parse/push-adapter from 3.4.1 to 4.1.2 ([#7897](https://github.com/parse-community/parse-server/issues/7897)) ([3d80ee5](https://github.com/parse-community/parse-server/commit/3d80ee5ec38169e16456a78932f3c38fcd225aae))
2022-03-28 02:57:17 +00:00
Snyk bot 3d80ee5ec3 fix: security upgrade @parse/push-adapter from 3.4.1 to 4.1.2 (#7897) 2022-03-28 04:56:02 +02:00
semantic-release-bot bf88869578 chore(release): 4.10.8 [skip ci]
## [4.10.8](https://github.com/parse-community/parse-server/compare/4.10.7...4.10.8) (2022-03-24)

### Bug Fixes

* sensitive keyword detection may produce false positives ([#7883](https://github.com/parse-community/parse-server/issues/7883)) ([d347613](https://github.com/parse-community/parse-server/commit/d34761369e3ad8358ef28abf3a3ec99467f1a958))
2022-03-24 01:50:33 +00:00
Manuel d34761369e fix: sensitive keyword detection may produce false positives (#7883) 2022-03-24 02:49:39 +01:00
Manuel 02f88f433e docs: add details to changelog (#7842) 2022-03-12 00:39:01 +01:00
semantic-release-bot 7c844772ea chore(release): 4.10.7 [skip ci]
## [4.10.7](https://github.com/parse-community/parse-server/compare/4.10.6...4.10.7) (2022-03-11)

### Bug Fixes

* security vulnerability that allows remote code execution (ghsa p6h4 93qp jhcm) ([#7841](https://github.com/parse-community/parse-server/issues/7841)) ([886bfd7](https://github.com/parse-community/parse-server/commit/886bfd7cac69496e3f73d4bb536f0eec3cba0e4d))
2022-03-11 23:20:36 +00:00
Manuel 886bfd7cac fix: security vulnerability that allows remote code execution (ghsa p6h4 93qp jhcm) (#7841) 2022-03-12 00:19:31 +01:00
Manuel 318c20319a ci: fix changelog file path (#7835) 2022-03-08 02:49:18 +01:00
Manuel 6f25ea93fb ci: add manual docker release workflow (#7809) 2022-02-13 00:16:02 +01:00
semantic-release-bot cd41626625 chore(release): 4.10.6 [skip ci]
## [4.10.6](https://github.com/parse-community/parse-server/compare/4.10.5...4.10.6) (2022-02-12)

### Bug Fixes

* update graphql dependencies to work with Parse Dashboard ([#7658](https://github.com/parse-community/parse-server/issues/7658)) ([350ecde](https://github.com/parse-community/parse-server/commit/350ecdee590f1b9d721895b2c79306c01622c3fc))
2022-02-12 16:08:06 +00:00
Frans Bouwmeester 350ecdee59 fix: update graphql dependencies to work with Parse Dashboard (#7658) 2022-02-12 17:07:04 +01:00
semantic-release-bot b465f7bade chore(release): 4.10.5 [skip ci]
## [4.10.5](https://github.com/parse-community/parse-server/compare/4.10.4...4.10.5) (2022-02-12)

### Bug Fixes

* security upgrade follow-redirects from 1.13.0 to 1.14.8 ([#7803](https://github.com/parse-community/parse-server/issues/7803)) ([611332e](https://github.com/parse-community/parse-server/commit/611332ea33831258efd3dd2f2c621c2e35fc95d3))
2022-02-12 15:42:56 +00:00
Manuel d7f2bb49d4 ci: disable auto-release dry-run 2022-02-12 16:41:32 +01:00
Manuel 4bd13088e1 docs: fix changelog typo 2022-02-12 16:37:58 +01:00
Snyk bot 611332ea33 fix: security upgrade follow-redirects from 1.13.0 to 1.14.8 (#7803) 2022-02-12 16:31:29 +01:00
Manuel 65c13febf5 ci: fix release automation workflow typo 2022-02-12 16:16:20 +01:00
Manuel adf7d928de ci: add release automation (#7807) 2022-02-12 16:14:28 +01:00
Frans Bouwmeester 065facdc53 test: fix failing tests on 4.x LTS branch (#7661) 2021-10-30 19:19:54 +02:00
dblythy 4ac4b7f710 Merge pull request from GHSA-7pr3-p5fm-8r9x
* fix: LQ deletes session token

* add 4.10.4

* add changes
2021-09-30 04:52:12 +02:00
Manuel 6683cd96b6 fix docker run (#7557) 2021-09-07 18:18:30 +02:00
Manuel ef2ec21355 ci: update docker image building (#7553)
* docker

* Update docker-publish.yml

* Update docker-publish.yml
2021-09-07 16:44:48 +02:00
Manuel c71ae7d478 chore(release): 4.10.3 (#7538) 2021-09-02 13:01:27 +02:00
Kartal Kaan Bozdoğan 6ae5835b19 Merge pull request from GHSA-xqp8-w826-hh6x
* Backport the advisory fix

* Added a 4.10.3 section to CHANGELOG
2021-09-02 12:46:48 +02:00
Manuel 0bfa6b7cc1 Release 4.10.2 (#7513)
* move graphql-tag from devDependencies to dependencies (#7183)

* bump version

* Update CHANGELOG.md
2021-08-24 00:46:39 +02:00
Manuel 0be0b87b2d bump version 2021-08-23 13:57:55 +02:00
Manuel f3133acf21 Release 4.10.1 (#7508)
* bump parse 3.3.0

* Update CHANGELOG.md

* update user test (PR #7464)

* fix Twitter API oauth Error (PR #7370)

* bumped dependencies

* Revert "bumped dependencies"

This reverts commit 97ad83dd15.

* bump @parse/push-adapter 3.4.1

* bump jwks-rsa@1.12.3

* bump mongodb@3.6.11

* bump ws@7.5.3

* changed logging for circular obj (PR #7457)

* Update CHANGELOG.md
2021-08-23 13:53:33 +02:00
Manuel 7e1da90bb2 added changelog 2021-08-20 19:56:37 +02:00
Manuel 0e3cae5c67 audit fix 2021-08-20 19:24:01 +02:00
Manuel f0d5232443 bumped version 2021-08-20 19:20:04 +02:00
Manuel a3483d82c8 fix changelog skip 4.5.1 2021-08-18 23:03:09 +02:00
Manuel 3c42584f59 4.5.2 2021-08-18 22:54:51 +02:00
Manuel 97b1dcad3b revert to version 4.5.0 for testing 2021-08-18 22:52:54 +02:00
Manuel 3a5c38d2e4 revert to version 4.5.0 for testing 2021-08-18 22:49:06 +02:00
Manuel 93d643e3eb change to RestWrite.createSession 2021-08-18 22:42:24 +02:00
Manuel 1306da7454 Merge pull request from GHSA-23r4-5mxp-c7g5 2021-08-18 22:24:29 +02:00
36 changed files with 6875 additions and 888 deletions
+4
View File
@@ -43,6 +43,8 @@ jobs:
PARSE_SERVER_TEST_CACHE: ${{ matrix.PARSE_SERVER_TEST_CACHE }}
NODE_VERSION: ${{ matrix.NODE_VERSION }}
steps:
- name: Fix usage of insecure GitHub protocol
run: sudo git config --system url."https://github".insteadOf "git://github"
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.NODE_VERSION }}
uses: actions/setup-node@v1
@@ -89,6 +91,8 @@ jobs:
POSTGRES_MAJOR_VERSION: 11
PARSE_SERVER_TEST_DATABASE_URI: postgres://postgres:postgres@localhost:5432/parse_server_postgres_adapter_test_database
steps:
- name: Fix usage of insecure GitHub protocol
run: sudo git config --system url."https://github".insteadOf "git://github"
- uses: actions/checkout@v2
- name: Use Node.js 10
uses: actions/setup-node@v1
+115
View File
@@ -0,0 +1,115 @@
name: release-automated
on:
push:
branches: [ release, alpha, beta, next-major, release-4.x.x ]
jobs:
release:
runs-on: ubuntu-latest
outputs:
current_tag: ${{ steps.tag.outputs.current_tag }}
trigger_branch: ${{ steps.branch.outputs.trigger_branch }}
steps:
- name: Determine trigger branch name
id: branch
run: echo "::set-output name=trigger_branch::${GITHUB_REF#refs/*/}"
- uses: actions/checkout@v2
with:
persist-credentials: false
- uses: actions/setup-node@v2
with:
node-version: 14
registry-url: https://registry.npmjs.org/
- name: Cache Node.js modules
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- run: npx semantic-release
env:
GH_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Determine tag on current commit
id: tag
run: echo "::set-output name=current_tag::$(git describe --tags --abbrev=0 --exact-match || echo '')"
docker:
needs: release
if: needs.release.outputs.current_tag != ''
env:
REGISTRY: docker.io
IMAGE_NAME: parseplatform/parse-server
runs-on: ubuntu-18.04
permissions:
contents: read
packages: write
steps:
- name: Determine branch name
id: branch
run: echo "::set-output name=branch_name::${GITHUB_REF#refs/*/}"
- name: Checkout repository
uses: actions/checkout@v2
with:
ref: ${{ needs.release.outputs.current_tag }}
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Log into Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=${{ steps.branch.outputs.branch_name == 'release' }}
tags: |
type=semver,pattern={{version}},value=${{ needs.release.outputs.current_tag }}
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64, linux/arm/v6, linux/arm/v7, linux/arm64/v8
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
docs:
needs: release
if: needs.release.outputs.current_tag != '' && github.ref == 'refs/heads/release'
runs-on: ubuntu-18.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 14
- name: Cache Node.js modules
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Generate Docs
run: |
echo $SOURCE_TAG
npm ci
./release_docs.sh
env:
SOURCE_TAG: ${{ needs.release.outputs.current_tag }}
- name: Deploy
uses: peaceiris/actions-gh-pages@v3.7.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
@@ -0,0 +1,57 @@
# Trigger this workflow only to manually create a Docker release; this should only be used
# in extraordinary circumstances, as Docker releases are normally created automatically as
# part of the automated release workflow.
name: release-manual-docker
on:
workflow_dispatch:
inputs:
ref:
default: ''
description: 'Reference (tag / SHA):'
env:
REGISTRY: docker.io
IMAGE_NAME: parseplatform/parse-server
jobs:
build:
runs-on: ubuntu-18.04
permissions:
contents: read
packages: write
steps:
- name: Determine branch name
id: branch
run: echo "::set-output name=branch_name::${GITHUB_REF#refs/*/}"
- name: Checkout repository
uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.ref }}
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Log into Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=${{ steps.branch.outputs.branch_name == 'release' && github.event.inputs.ref == '' }}
tags: |
type=semver,enable=true,pattern={{version}},value=${{ github.event.inputs.ref }}
type=raw,enable=${{ github.event.inputs.ref == '' }},value=latest
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64, linux/arm/v6, linux/arm/v7, linux/arm64/v8
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
-63
View File
@@ -1,63 +0,0 @@
name: release
on:
release:
types: [published]
jobs:
publish-npm:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '10.14'
registry-url: https://registry.npmjs.org/
- name: Cache Node.js modules
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
publish-docs:
runs-on: ubuntu-18.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '10.14'
- name: Cache Node.js modules
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Get Tag
uses: actions/github-script@v3
id: tag
with:
github-token: ${{secrets.GITHUB_TOKEN}}
result-encoding: string
script: |
const ref = process.env.GITHUB_REF
if(!ref.startsWith('refs/tags/'))
return ''
return ref.replace(/^refs\/tags\//, '')
- name: Generate Docs
run: |
echo $SOURCE_TAG
npm ci
./release_docs.sh
env:
SOURCE_TAG: ${{ steps.tag.outputs.result }}
- name: Deploy
uses: peaceiris/actions-gh-pages@v3.7.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
+61
View File
@@ -0,0 +1,61 @@
*{{#if scope}} **{{scope}}:**
{{~/if}} {{#if subject}}
{{~subject}}
{{~else}}
{{~header}}
{{~/if}}
{{~!-- commit link --}} {{#if @root.linkReferences~}}
([{{shortHash}}](
{{~#if @root.repository}}
{{~#if @root.host}}
{{~@root.host}}/
{{~/if}}
{{~#if @root.owner}}
{{~@root.owner}}/
{{~/if}}
{{~@root.repository}}
{{~else}}
{{~@root.repoUrl}}
{{~/if}}/
{{~@root.commit}}/{{hash}}))
{{~else}}
{{~shortHash}}
{{~/if}}
{{~!-- commit references --}}
{{~#if references~}}
, closes
{{~#each references}} {{#if @root.linkReferences~}}
[
{{~#if this.owner}}
{{~this.owner}}/
{{~/if}}
{{~this.repository}}#{{this.issue}}](
{{~#if @root.repository}}
{{~#if @root.host}}
{{~@root.host}}/
{{~/if}}
{{~#if this.repository}}
{{~#if this.owner}}
{{~this.owner}}/
{{~/if}}
{{~this.repository}}
{{~else}}
{{~#if @root.owner}}
{{~@root.owner}}/
{{~/if}}
{{~@root.repository}}
{{~/if}}
{{~else}}
{{~@root.repoUrl}}
{{~/if}}/
{{~@root.issue}}/{{this.issue}})
{{~else}}
{{~#if this.owner}}
{{~this.owner}}/
{{~/if}}
{{~this.repository}}#{{this.issue}}
{{~/if}}{{/each}}
{{~/if}}
+11
View File
@@ -0,0 +1,11 @@
{{#if noteGroups}}
{{#each noteGroups}}
### {{title}}
{{#each notes}}
* {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{text}} ([{{commit.shortHash}}]({{commit.shortHash}}))
{{/each}}
{{/each}}
{{/if}}
+25
View File
@@ -0,0 +1,25 @@
{{#if isPatch~}}
##
{{~else~}}
#
{{~/if}} {{#if @root.linkCompare~}}
[{{version}}](
{{~#if @root.repository~}}
{{~#if @root.host}}
{{~@root.host}}/
{{~/if}}
{{~#if @root.owner}}
{{~@root.owner}}/
{{~/if}}
{{~@root.repository}}
{{~else}}
{{~@root.repoUrl}}
{{~/if~}}
/compare/{{previousTag}}...{{currentTag}})
{{~else}}
{{~version}}
{{~/if}}
{{~#if title}} "{{title}}"
{{~/if}}
{{~#if date}} ({{date}})
{{/if}}
+14
View File
@@ -0,0 +1,14 @@
{{> header}}
{{#each commitGroups}}
{{#if title}}
### {{title}}
{{/if}}
{{#each commits}}
{{> commit root=@root}}
{{/each}}
{{/each}}
{{> footer}}
+100 -4
View File
@@ -1,9 +1,105 @@
## Parse Server Changelog
## [4.10.11](https://github.com/parse-community/parse-server/compare/4.10.10...4.10.11) (2022-06-17)
### master
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.5.0...master)
### 4.5.0
### Bug Fixes
* certificate in Apple Game Center auth adapter not validated; this fixes a security vulnerability in which authentication could be bypassed using a fake certificate; if you are using the Apple Gamer Center auth adapter it is your responsibility to keep its root certificate up-to-date and we advice you read the security advisory ([GHSA-rh9j-f5f8-rvgc](https://github.com/parse-community/parse-server/security/advisories/GHSA-rh9j-f5f8-rvgc)) ([145838d](https://github.com/parse-community/parse-server/commit/145838d2d9c1ecf76412a962a4ef61c712bcb0a7))
## [4.10.10](https://github.com/parse-community/parse-server/compare/4.10.9...4.10.10) (2022-05-01)
### Bug Fixes
* authentication bypass and denial of service (DoS) vulnerabilities in Apple Game Center auth adapter (GHSA-qf8x-vqjv-92gr) ([#7963](https://github.com/parse-community/parse-server/issues/7963)) ([1930a64](https://github.com/parse-community/parse-server/commit/1930a64e9c7e0d287b4ad9e57c6ee03b11b9cbc4))
## [4.10.9](https://github.com/parse-community/parse-server/compare/4.10.8...4.10.9) (2022-03-28)
### Bug Fixes
* security upgrade @parse/push-adapter from 3.4.1 to 4.1.2 ([#7897](https://github.com/parse-community/parse-server/issues/7897)) ([3d80ee5](https://github.com/parse-community/parse-server/commit/3d80ee5ec38169e16456a78932f3c38fcd225aae))
## [4.10.8](https://github.com/parse-community/parse-server/compare/4.10.7...4.10.8) (2022-03-24)
### Bug Fixes
* sensitive keyword detection may produce false positives ([#7883](https://github.com/parse-community/parse-server/issues/7883)) ([d347613](https://github.com/parse-community/parse-server/commit/d34761369e3ad8358ef28abf3a3ec99467f1a958))
## [4.10.7](https://github.com/parse-community/parse-server/compare/4.10.6...4.10.7) (2022-03-11)
### Bug Fixes
* security vulnerability that allows remote code execution ([GHSA-p6h4-93qp-jhcm](https://github.com/parse-community/parse-server/security/advisories/GHSA-p6h4-93qp-jhcm)) ([#7841](https://github.com/parse-community/parse-server/issues/7841)) ([886bfd7](https://github.com/parse-community/parse-server/commit/886bfd7cac69496e3f73d4bb536f0eec3cba0e4d))
Note that as part of the fix a new security feature scans for sensitive keywords in request data to prevent JavaScript prototype pollution. If such a keyword is found, the request is rejected with HTTP response code `400` and Parse Error `105` (`INVALID_KEY_NAME`). By default these keywords are: `{_bsontype: "Code"}`, `constructor`, `__proto__`. If you are using any of these keywords in your request data, you can override the default keywords by setting the new Parse Server option `requestKeywordDenylist` to `[]` and specify your own keywords as needed.
## [4.10.6](https://github.com/parse-community/parse-server/compare/4.10.5...4.10.6) (2022-02-12)
### Bug Fixes
* update graphql dependencies to work with Parse Dashboard ([#7658](https://github.com/parse-community/parse-server/issues/7658)) ([350ecde](https://github.com/parse-community/parse-server/commit/350ecdee590f1b9d721895b2c79306c01622c3fc))
## [4.10.5](https://github.com/parse-community/parse-server/compare/4.10.4...4.10.5) (2022-02-12)
### Bug Fixes
* security upgrade follow-redirects from 1.13.0 to 1.14.8 ([#7803](https://github.com/parse-community/parse-server/issues/7803)) ([611332e](https://github.com/parse-community/parse-server/commit/611332ea33831258efd3dd2f2c621c2e35fc95d3))
# 4.10.4
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.10.3...4.10.4)
## Security Fixes
- Strip out sessionToken when LiveQuery is used on Parse.User (Daniel Blyth) [GHSA-7pr3-p5fm-8r9x](https://github.com/parse-community/parse-server/security/advisories/GHSA-7pr3-p5fm-8r9x)
# 4.10.3
## 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:__
+18 -1
View File
@@ -1,15 +1,30 @@
############################################################
# Build stage
############################################################
FROM node:lts-alpine as build
RUN apk update; \
apk add git;
WORKDIR /tmp
# Copy package.json first to benefit from layer caching
COPY package*.json ./
RUN npm ci
# Copy src to have config files for install
COPY . .
# Clean npm cache; added to fix an issue with the install process
RUN npm cache clean --force
# Install all dependencies
RUN npm ci
# Run build steps
RUN npm run build
############################################################
# Release stage
############################################################
FROM node:lts-alpine as release
RUN apk update; \
@@ -21,6 +36,8 @@ WORKDIR /parse-server
COPY package*.json ./
# Clean npm cache; added to fix an issue with the install process
RUN npm cache clean --force
RUN npm ci --production --ignore-scripts
COPY bin bin
+5300 -646
View File
File diff suppressed because it is too large Load Diff
+25 -17
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "4.5.0",
"version": "4.10.11",
"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": "4.1.2",
"@parse/s3-files-adapter": "1.6.2",
"@parse/simple-mailgun-adapter": "1.1.0",
"apollo-server-express": "2.19.0",
"bcryptjs": "2.4.3",
@@ -34,33 +34,34 @@
"cors": "2.8.5",
"deepcopy": "2.1.0",
"express": "4.17.1",
"follow-redirects": "1.13.0",
"graphql": "15.4.0",
"follow-redirects": "1.14.8",
"graphql": "15.5.1",
"graphql-list-fields": "2.0.2",
"graphql-relay": "0.6.0",
"graphql-relay": "0.7.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,16 +82,23 @@
"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",
"jasmine-spec-reporter": "7.0.0",
"jsdoc": "3.6.7",
"jsdoc-babel": "0.5.0",
"lint-staged": "10.2.3",
"mongodb-runner": "mongodb-js/runner",
"node-fetch": "2.6.1",
"nyc": "15.1.0",
"prettier": "2.0.5"
"prettier": "2.0.5",
"@semantic-release/changelog": "5.0.1",
"@semantic-release/commit-analyzer": "8.0.1",
"@semantic-release/git": "9.0.0",
"@semantic-release/github": "7.2.3",
"@semantic-release/npm": "7.1.3",
"@semantic-release/release-notes-generator": "9.0.3",
"semantic-release": "17.4.6"
},
"scripts": {
"definitions": "node ./resources/buildConfigDefinitions.js",
+111
View File
@@ -0,0 +1,111 @@
/**
* Semantic Release Config
*/
const fs = require('fs').promises;
const path = require('path');
// Get env vars
const ref = process.env.GITHUB_REF;
const serverUrl = process.env.GITHUB_SERVER_URL;
const repository = process.env.GITHUB_REPOSITORY;
const repositoryUrl = serverUrl + '/' + repository;
// Declare params
const resourcePath = './.releaserc/';
const templates = {
main: { file: 'template.hbs', text: undefined },
header: { file: 'header.hbs', text: undefined },
commit: { file: 'commit.hbs', text: undefined },
footer: { file: 'footer.hbs', text: undefined },
};
// Declare semantic config
async function config() {
// Get branch
const branch = ref.split('/').pop();
console.log(`Running on branch: ${branch}`);
// Set changelog file
const changelogFile = `./CHANGELOG.md`;
console.log(`Changelog file output to: ${changelogFile}`);
// Load template file contents
await loadTemplates();
const config = {
branches: [
'release',
{ name: 'alpha', prerelease: true },
{ name: 'beta', prerelease: true },
'next-major',
// Long-Term-Support branches
{ name: 'release-4.x.x', range: '4.x.x', channel: '4.x.x' },
],
dryRun: false,
debug: true,
ci: true,
tagFormat: '${version}',
plugins: [
['@semantic-release/commit-analyzer', {
preset: 'angular',
releaseRules: [
{ type: 'docs', scope: 'README', release: 'patch' },
{ scope: 'no-release', release: false },
],
parserOpts: {
noteKeywords: [ 'BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING' ],
},
}],
['@semantic-release/release-notes-generator', {
preset: 'angular',
parserOpts: {
noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING']
},
writerOpts: {
commitsSort: ['subject', 'scope'],
mainTemplate: templates.main.text,
headerPartial: templates.header.text,
commitPartial: templates.commit.text,
footerPartial: templates.footer.text,
},
}],
['@semantic-release/changelog', {
'changelogFile': changelogFile,
}],
['@semantic-release/npm', {
'npmPublish': true,
}],
['@semantic-release/git', {
assets: [changelogFile, 'package.json', 'package-lock.json', 'npm-shrinkwrap.json'],
}],
['@semantic-release/github', {
successComment: getReleaseComment(),
labels: ['type:ci'],
releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
}],
],
};
return config;
}
async function loadTemplates() {
for (const template of Object.keys(templates)) {
const text = await readFile(path.resolve(__dirname, resourcePath, templates[template].file));
templates[template].text = text;
}
}
async function readFile(filePath) {
return await fs.readFile(filePath, 'utf-8');
}
function getReleaseComment() {
const url = repositoryUrl + '/releases/tag/${nextRelease.gitTag}';
const comment = '🎉 This change has been released in version [${nextRelease.version}](' + url + ')';
return comment;
}
module.exports = config();
+14
View File
@@ -150,6 +150,20 @@ function parseDefaultValue(elt, value, t) {
literalValue = t.arrayExpression(array.map((value) => {
if (typeof value == 'string') {
return t.stringLiteral(value);
} else if (typeof value == 'number') {
return t.numericLiteral(value);
} else if (typeof value == 'object') {
const object = parsers.objectParser(value);
const props = Object.entries(object).map(([k, v]) => {
if (typeof v == 'string') {
return t.objectProperty(t.identifier(k), t.stringLiteral(v));
} else if (typeof v == 'number') {
return t.objectProperty(t.identifier(k), t.numericLiteral(v));
} else if (typeof v == 'boolean') {
return t.objectProperty(t.identifier(k), t.booleanLiteral(v));
}
});
return t.objectExpression(props);
} else {
throw new Error('Unable to parse array');
}
+162 -30
View File
@@ -1644,8 +1644,41 @@ describe('apple signin auth adapter', () => {
describe('Apple Game Center Auth adapter', () => {
const gcenter = require('../lib/Adapters/Auth/gcenter');
const fs = require('fs');
const testCert = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
it('can load adapter', async () => {
const options = {
gcenter: {
rootCertificateUrl:
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
});
it('validateAuthData should validate', async () => {
const options = {
gcenter: {
rootCertificateUrl:
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
// real token is used
const authData = {
id: 'G:1965586982',
@@ -1656,48 +1689,147 @@ describe('Apple Game Center Auth adapter', () => {
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
};
try {
await gcenter.validateAuthData(authData);
} catch (e) {
fail();
}
gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
await gcenter.validateAuthData(authData);
});
it('validateAuthData invalid signature id', async () => {
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
{}
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
const authData = {
id: 'G:1965586982',
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-6.cer',
timestamp: 1565257031287,
signature: '1234',
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
bundleId: 'com.example.com',
};
try {
await gcenter.validateAuthData(authData);
fail();
} catch (e) {
expect(e.message).toBe('Apple Game Center - invalid signature');
}
await expectAsync(gcenter.validateAuthData(authData)).toBeRejectedWith(
new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Apple Game Center - invalid signature')
);
});
it('validateAuthData invalid public key url', async () => {
const authData = {
id: 'G:1965586982',
publicKeyUrl: 'invalid.com',
timestamp: 1565257031287,
signature: '1234',
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
it('validateAuthData invalid public key http url', async () => {
const options = {
gcenter: {
rootCertificateUrl:
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
const publicKeyUrls = [
'example.com',
'http://static.gc.apple.com/public-key/gc-prod-4.cer',
'https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg',
'https://example.com/ \\.apple.com/public_key.cer',
'https://example.com/ &.apple.com/public_key.cer',
];
await Promise.all(
publicKeyUrls.map(publicKeyUrl =>
expectAsync(
gcenter.validateAuthData({
id: 'G:1965586982',
timestamp: 1565257031287,
publicKeyUrl,
signature: '1234',
salt: 'DzqqrQ==',
bundleId: 'com.example.com',
})
).toBeRejectedWith(
new Parse.Error(
Parse.Error.SCRIPT_FAILED,
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
)
)
)
);
});
try {
await gcenter.validateAuthData(authData);
fail();
} catch (e) {
expect(e.message).toBe('Apple Game Center - invalid publicKeyUrl: invalid.com');
}
it('should not validate Symantec Cert', async () => {
const options = {
gcenter: {
rootCertificateUrl:
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
expect(() =>
gcenter.verifyPublicKeyIssuer(
testCert,
'https://static.gc.apple.com/public-key/gc-prod-4.cer'
)
);
});
it('adapter should load default cert', async () => {
const options = {
gcenter: {},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
const previous = new Date();
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
const duration = new Date().getTime() - previous.getTime();
expect(duration).toEqual(0);
});
it('adapter should throw', async () => {
const options = {
gcenter: {
rootCertificateUrl: 'https://example.com',
},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await expectAsync(
adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
)
).toBeRejectedWith(
new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
)
);
});
});
+15 -7
View File
@@ -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',
+46
View File
@@ -928,6 +928,52 @@ describe('ParseLiveQuery', function () {
done();
});
it('should strip out session token in LiveQuery', async () => {
await reconfigureServer({
liveQuery: { classNames: ['_User'] },
startLiveQueryServer: true,
verbose: false,
silent: true,
});
const user = new Parse.User();
user.setUsername('username');
user.setPassword('password');
user.set('foo', 'bar');
const query = new Parse.Query(Parse.User);
query.equalTo('foo', 'bar');
const subscription = await query.subscribe();
const events = ['create', 'update', 'enter', 'leave', 'delete'];
const response = (obj, prev) => {
expect(obj.get('sessionToken')).toBeUndefined();
expect(obj.sessionToken).toBeUndefined();
expect(prev && prev.sessionToken).toBeUndefined();
if (prev && prev.get) {
expect(prev.get('sessionToken')).toBeUndefined();
}
};
const calls = {};
for (const key of events) {
calls[key] = response;
spyOn(calls, key).and.callThrough();
subscription.on(key, calls[key]);
}
await user.signUp();
user.unset('foo');
await user.save();
user.set('foo', 'bar');
await user.save();
user.set('yolo', 'bar');
await user.save();
await user.destroy();
await new Promise(resolve => setTimeout(resolve, 10));
for (const key of events) {
expect(calls[key]).toHaveBeenCalled();
}
});
afterEach(async function (done) {
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
client.close();
+48
View File
@@ -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' });
+104 -52
View File
@@ -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 => {
@@ -3925,6 +3929,54 @@ describe('Parse.User testing', () => {
}
});
it('should strip out authdata in LiveQuery', async () => {
const provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
await reconfigureServer({
liveQuery: { classNames: ['_User'] },
startLiveQueryServer: true,
verbose: false,
silent: true,
});
const query = new Parse.Query(Parse.User);
query.doesNotExist('foo');
const subscription = await query.subscribe();
const events = ['create', 'update', 'enter', 'leave', 'delete'];
const response = (obj, prev) => {
expect(obj.get('authData')).toBeUndefined();
expect(obj.authData).toBeUndefined();
expect(prev && prev.authData).toBeUndefined();
if (prev && prev.get) {
expect(prev.get('authData')).toBeUndefined();
}
};
const calls = {};
for (const key of events) {
calls[key] = response;
spyOn(calls, key).and.callThrough();
subscription.on(key, calls[key]);
}
const user = await Parse.User._logInWith('facebook');
user.set('foo', 'bar');
await user.save();
user.unset('foo');
await user.save();
user.set('yolo', 'bar');
await user.save();
await user.destroy();
await new Promise(resolve => setTimeout(resolve, 10));
for (const key of events) {
expect(calls[key]).toHaveBeenCalled();
}
subscription.unsubscribe();
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
client.close();
await new Promise(resolve => setTimeout(resolve, 10));
});
describe('issue #4897', () => {
it_only_db('mongo')('should be able to login with a legacy user (no ACL)', async () => {
// This issue is a side effect of the locked users and legacy users which don't have ACL's
+5
View File
@@ -1,6 +1,11 @@
'use strict';
const CurrentSpecReporter = require('./support/CurrentSpecReporter.js');
const { SpecReporter } = require('jasmine-spec-reporter');
// Sets up a Parse API server for testing.
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 5000;
jasmine.getEnv().addReporter(new CurrentSpecReporter());
jasmine.getEnv().addReporter(new SpecReporter());
global.on_db = (db, callback, elseCallback) => {
if (process.env.PARSE_SERVER_TEST_DB == db) {
+15
View File
@@ -0,0 +1,15 @@
// Sets a global variable to the current test spec
// ex: global.currentSpec.description
global.currentSpec = null;
class CurrentSpecReporter {
specStarted(spec) {
global.currentSpec = spec;
}
specDone() {
global.currentSpec = null;
}
}
module.exports = CurrentSpecReporter;
+28
View File
@@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIEvDCCA6SgAwIBAgIQXRHxNXkw1L9z5/3EZ/T/hDANBgkqhkiG9w0BAQsFADB/
MQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAd
BgNVBAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxMDAuBgNVBAMTJ1N5bWFudGVj
IENsYXNzIDMgU0hBMjU2IENvZGUgU2lnbmluZyBDQTAeFw0xODA5MTcwMDAwMDBa
Fw0xOTA5MTcyMzU5NTlaMHMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9y
bmlhMRIwEAYDVQQHDAlDdXBlcnRpbm8xFDASBgNVBAoMC0FwcGxlLCBJbmMuMQ8w
DQYDVQQLDAZHQyBTUkUxFDASBgNVBAMMC0FwcGxlLCBJbmMuMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA06fwIi8fgKrTQu7cBcFkJVF6+Tqvkg7MKJTM
IOYPPQtPF3AZYPsbUoRKAD7/JXrxxOSVJ7vU1mP77tYG8TcUteZ3sAwvt2dkRbm7
ZO6DcmSggv1Dg4k3goNw4GYyCY4Z2/8JSmsQ80Iv/UOOwynpBziEeZmJ4uck6zlA
17cDkH48LBpKylaqthym5bFs9gj11pto7mvyb5BTcVuohwi6qosvbs/4VGbC2Nsz
ie416nUZfv+xxoXH995gxR2mw5cDdeCew7pSKxEhvYjT2nVdQF0q/hnPMFnOaEyT
q79n3gwFXyt0dy8eP6KBF7EW9J6b7ubu/j7h+tQfxPM+gTXOBQIDAQABo4IBPjCC
ATowCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH
AwMwYQYDVR0gBFowWDBWBgZngQwBBAEwTDAjBggrBgEFBQcCARYXaHR0cHM6Ly9k
LnN5bWNiLmNvbS9jcHMwJQYIKwYBBQUHAgIwGQwXaHR0cHM6Ly9kLnN5bWNiLmNv
bS9ycGEwHwYDVR0jBBgwFoAUljtT8Hkzl699g+8uK8zKt4YecmYwKwYDVR0fBCQw
IjAgoB6gHIYaaHR0cDovL3N2LnN5bWNiLmNvbS9zdi5jcmwwVwYIKwYBBQUHAQEE
SzBJMB8GCCsGAQUFBzABhhNodHRwOi8vc3Yuc3ltY2QuY29tMCYGCCsGAQUFBzAC
hhpodHRwOi8vc3Yuc3ltY2IuY29tL3N2LmNydDANBgkqhkiG9w0BAQsFAAOCAQEA
I/j/PcCNPebSAGrcqSFBSa2mmbusOX01eVBg8X0G/z8Z+ZWUfGFzDG0GQf89MPxV
woec+nZuqui7o9Bg8s8JbHV0TC52X14CbTj9w/qBF748WbH9gAaTkrJYPm+MlNhu
tjEuQdNl/YXVMvQW4O8UMHTi09GyJQ0NC4q92Wxvx1m/qzjvTLvrXHGQ9pEHhPyz
vfBLxQkWpNoCNKU7UeESyH06XOrGc9MsII9deeKsDJp9a0jtx+pP4MFVtFME9SSQ
tMBs0It7WwEf7qcRLpialxKwY2EzQ9g4WnANHqo18PrDBE10TFpZPzUh7JhMViVr
EEbl0YdElmF8Hlamah/yNw==
-----END CERTIFICATE-----
+297
View File
@@ -0,0 +1,297 @@
const request = require('../lib/request');
describe('Vulnerabilities', () => {
describe('Object prototype pollution', () => {
it('denies object prototype to be polluted with keyword "constructor"', async () => {
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
headers: headers,
method: 'POST',
url: 'http://localhost:8378/1/classes/PP',
body: JSON.stringify({
obj: {
constructor: {
prototype: {
dummy: 0,
},
},
},
}),
}).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":"constructor"}.');
expect(Object.prototype.dummy).toBeUndefined();
});
it('denies object prototype to be polluted with keypath string "constructor"', async () => {
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const objResponse = await request({
headers: headers,
method: 'POST',
url: 'http://localhost:8378/1/classes/PP',
body: JSON.stringify({
obj: {},
}),
}).catch(e => e);
const pollResponse = await request({
headers: headers,
method: 'PUT',
url: `http://localhost:8378/1/classes/PP/${objResponse.data.objectId}`,
body: JSON.stringify({
'obj.constructor.prototype.dummy': {
__op: 'Increment',
amount: 1,
},
}),
}).catch(e => e);
expect(Object.prototype.dummy).toBeUndefined();
expect(pollResponse.status).toBe(400);
const text = JSON.parse(pollResponse.text);
expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME);
expect(text.error).toBe('Prohibited keyword in request data: {"key":"constructor"}.');
expect(Object.prototype.dummy).toBeUndefined();
});
it('denies object prototype to be polluted with keyword "__proto__"', async () => {
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
headers: headers,
method: 'POST',
url: 'http://localhost:8378/1/classes/PP',
body: JSON.stringify({ 'obj.__proto__.dummy': 0 }),
}).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":"__proto__"}.');
expect(Object.prototype.dummy).toBeUndefined();
});
});
describe('Request denylist', () => {
it('denies BSON type code data in write request by default', async () => {
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const params = {
headers: headers,
method: 'POST',
url: 'http://localhost:8378/1/classes/RCE',
body: JSON.stringify({
obj: {
_bsontype: 'Code',
code: 'delete Object.prototype.evalFunctions',
},
}),
};
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('allows BSON type code data in write request with custom denylist', async () => {
await reconfigureServer({
requestKeywordDenylist: [],
});
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const params = {
headers: headers,
method: 'POST',
url: 'http://localhost:8378/1/classes/RCE',
body: JSON.stringify({
obj: {
_bsontype: 'Code',
code: 'delete Object.prototype.evalFunctions',
},
}),
};
const response = await request(params).catch(e => e);
expect(response.status).toBe(201);
const text = JSON.parse(response.text);
expect(text.objectId).toBeDefined();
});
it('denies write request with custom denylist of key/value', async () => {
await reconfigureServer({
requestKeywordDenylist: [{ key: 'a[K]ey', value: 'aValue[123]*' }],
});
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const params = {
headers: headers,
method: 'POST',
url: 'http://localhost:8378/1/classes/RCE',
body: JSON.stringify({
obj: {
aKey: 'aValue321',
code: 'delete Object.prototype.evalFunctions',
},
}),
};
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":"a[K]ey","value":"aValue[123]*"}.'
);
});
it('denies write request with custom denylist of nested key/value', async () => {
await reconfigureServer({
requestKeywordDenylist: [{ key: 'a[K]ey', value: 'aValue[123]*' }],
});
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const params = {
headers: headers,
method: 'POST',
url: 'http://localhost:8378/1/classes/RCE',
body: JSON.stringify({
obj: {
nested: {
aKey: 'aValue321',
code: 'delete Object.prototype.evalFunctions',
},
},
}),
};
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":"a[K]ey","value":"aValue[123]*"}.'
);
});
it('denies write request with custom denylist of key/value in array', async () => {
await reconfigureServer({
requestKeywordDenylist: [{ key: 'a[K]ey', value: 'aValue[123]*' }],
});
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const params = {
headers: headers,
method: 'POST',
url: 'http://localhost:8378/1/classes/RCE',
body: JSON.stringify({
obj: [
{
aKey: 'aValue321',
code: 'delete Object.prototype.evalFunctions',
},
],
}),
};
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":"a[K]ey","value":"aValue[123]*"}.'
);
});
it('denies write request with custom denylist of key', async () => {
await reconfigureServer({
requestKeywordDenylist: [{ key: 'a[K]ey' }],
});
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const params = {
headers: headers,
method: 'POST',
url: 'http://localhost:8378/1/classes/RCE',
body: JSON.stringify({
obj: {
aKey: 'aValue321',
code: 'delete Object.prototype.evalFunctions',
},
}),
};
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":"a[K]ey"}.');
});
it('denies write request with custom denylist of value', async () => {
await reconfigureServer({
requestKeywordDenylist: [{ value: 'aValue[123]*' }],
});
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const params = {
headers: headers,
method: 'POST',
url: 'http://localhost:8378/1/classes/RCE',
body: JSON.stringify({
obj: {
aKey: 'aValue321',
code: 'delete Object.prototype.evalFunctions',
},
}),
};
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: {"value":"aValue[123]*"}.');
});
});
describe('Ignore non-matches', () => {
it('ignores write request that contains only fraction of denied keyword', async () => {
await reconfigureServer({
requestKeywordDenylist: [{ key: 'abc' }],
});
// Initially saving an object executes the keyword detection in RestWrite.js
const obj = new TestObject({ a: { b: { c: 0 } } });
await expectAsync(obj.save()).toBeResolved();
// Modifying a nested key executes the keyword detection in DatabaseController.js
obj.increment('a.b.c');
await expectAsync(obj.save()).toBeResolved();
});
});
});
+100 -26
View File
@@ -14,20 +14,17 @@ const authData = {
const { Parse } = require('parse/node');
const crypto = require('crypto');
const https = require('https');
const url = require('url');
const { pki } = require('node-forge');
const ca = { cert: null, url: null };
const cache = {}; // (publicKey -> cert) cache
function verifyPublicKeyUrl(publicKeyUrl) {
const parsedUrl = url.parse(publicKeyUrl);
if (parsedUrl.protocol !== 'https:') {
try {
const regex = /^https:\/\/(?:[-_A-Za-z0-9]+\.){0,}apple\.com\/.*\.cer$/;
return regex.test(publicKeyUrl);
} catch (error) {
return false;
}
const hostnameParts = parsedUrl.hostname.split('.');
const length = hostnameParts.length;
const domainParts = hostnameParts.slice(length - 2, length);
const domain = domainParts.join('.');
return domain === 'apple.com';
}
function convertX509CertToPEM(X509Cert) {
@@ -40,7 +37,7 @@ function convertX509CertToPEM(X509Cert) {
return pemPreFix + certBody + pemPostFix;
}
function getAppleCertificate(publicKeyUrl) {
async function getAppleCertificate(publicKeyUrl) {
if (!verifyPublicKeyUrl(publicKeyUrl)) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
@@ -50,26 +47,59 @@ function getAppleCertificate(publicKeyUrl) {
if (cache[publicKeyUrl]) {
return cache[publicKeyUrl];
}
const url = new URL(publicKeyUrl);
const headOptions = {
hostname: url.hostname,
path: url.pathname,
method: 'HEAD',
};
const cert_headers = await new Promise((resolve, reject) =>
https.get(headOptions, res => resolve(res.headers)).on('error', reject)
);
const validContentTypes = ['application/x-x509-ca-cert', 'application/pkix-cert'];
if (
!validContentTypes.includes(cert_headers['content-type']) ||
cert_headers['content-length'] == null ||
cert_headers['content-length'] > 10000
) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
);
}
const { certificate, headers } = await getCertificate(publicKeyUrl);
if (headers['cache-control']) {
const expire = headers['cache-control'].match(/max-age=([0-9]+)/);
if (expire) {
cache[publicKeyUrl] = certificate;
// we'll expire the cache entry later, as per max-age
setTimeout(() => {
delete cache[publicKeyUrl];
}, parseInt(expire[1], 10) * 1000);
}
}
return verifyPublicKeyIssuer(certificate, publicKeyUrl);
}
function getCertificate(url, buffer) {
return new Promise((resolve, reject) => {
https
.get(publicKeyUrl, res => {
let data = '';
.get(url, res => {
const data = [];
res.on('data', chunk => {
data += chunk.toString('base64');
data.push(chunk);
});
res.on('end', () => {
const cert = convertX509CertToPEM(data);
if (res.headers['cache-control']) {
var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/);
if (expire) {
cache[publicKeyUrl] = cert;
// we'll expire the cache entry later, as per max-age
setTimeout(() => {
delete cache[publicKeyUrl];
}, parseInt(expire[1], 10) * 1000);
}
if (buffer) {
resolve({ certificate: Buffer.concat(data), headers: res.headers });
return;
}
resolve(cert);
let cert = '';
for (const chunk of data) {
cert += chunk.toString('base64');
}
const certificate = convertX509CertToPEM(cert);
resolve({ certificate, headers: res.headers });
});
})
.on('error', reject);
@@ -100,6 +130,30 @@ function verifySignature(publicKey, authData) {
}
}
function verifyPublicKeyIssuer(cert, publicKeyUrl) {
const publicKeyCert = pki.certificateFromPem(cert);
if (!ca.cert) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
);
}
try {
if (!ca.cert.verify(publicKeyCert)) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
);
}
} catch (e) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
);
}
return cert;
}
// Returns a promise that fulfills if this user id is valid.
async function validateAuthData(authData) {
if (!authData.id) {
@@ -111,11 +165,31 @@ async function validateAuthData(authData) {
}
// Returns a promise that fulfills if this app id is valid.
function validateAppId() {
return Promise.resolve();
async function validateAppId(appIds, authData, options = {}) {
if (!options.rootCertificateUrl) {
options.rootCertificateUrl =
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem';
}
if (ca.url === options.rootCertificateUrl) {
return;
}
const { certificate, headers } = await getCertificate(options.rootCertificateUrl, true);
if (
headers['content-type'] !== 'application/x-pem-file' ||
headers['content-length'] == null ||
headers['content-length'] > 10000
) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
);
}
ca.cert = pki.certificateFromPem(certificate);
ca.url = options.rootCertificateUrl;
}
module.exports = {
validateAppId,
validateAuthData,
cache,
};
@@ -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) {
+15 -1
View File
@@ -33,7 +33,11 @@ export class Config {
cacheInfo.schemaCacheTTL,
cacheInfo.enableSingleSchemaCache
);
config.database = new DatabaseController(cacheInfo.databaseController.adapter, schemaCache);
config.database = new DatabaseController(
cacheInfo.databaseController.adapter,
schemaCache,
config
);
} else {
config[key] = cacheInfo[key];
}
@@ -71,6 +75,7 @@ export class Config {
allowHeaders,
idempotencyOptions,
emailVerifyTokenReuseIfValid,
requestKeywordDenylist,
}) {
if (masterKey === readOnlyMasterKey) {
throw new Error('masterKey and readOnlyMasterKey should be different');
@@ -105,6 +110,15 @@ export class Config {
this.validateMaxLimit(maxLimit);
this.validateAllowHeaders(allowHeaders);
this.validateIdempotencyOptions(idempotencyOptions);
this.validateRequestKeywordDenylist(requestKeywordDenylist);
}
static validateRequestKeywordDenylist(requestKeywordDenylist) {
if (requestKeywordDenylist === undefined) {
requestKeywordDenylist = requestKeywordDenylist.default;
} else if (!Array.isArray(requestKeywordDenylist)) {
throw 'Parse Server option requestKeywordDenylist must be an array.';
}
}
static validateIdempotencyOptions(idempotencyOptions) {
+61 -38
View File
@@ -11,8 +11,10 @@ import intersect from 'intersect';
// @flow-disable-next
import deepcopy from 'deepcopy';
import logger from '../logger';
import Utils from '../Utils';
import * as SchemaController from './SchemaController';
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
import type { ParseServerOptions } from '../Options';
import type { QueryOptions, FullQueryOptions } from '../Adapters/Storage/StorageAdapter';
function addWriteACL(query, acl) {
@@ -257,41 +259,6 @@ const isSpecialUpdateKey = key => {
return specialKeysForUpdate.indexOf(key) >= 0;
};
function expandResultOnKeyPath(object, key, value) {
if (key.indexOf('.') < 0) {
object[key] = value[key];
return object;
}
const path = key.split('.');
const firstKey = path[0];
const nextPath = path.slice(1).join('.');
object[firstKey] = expandResultOnKeyPath(object[firstKey] || {}, nextPath, value[firstKey]);
delete object[key];
return object;
}
function sanitizeDatabaseResult(originalObject, result): Promise<any> {
const response = {};
if (!result) {
return Promise.resolve(response);
}
Object.keys(originalObject).forEach(key => {
const keyUpdate = originalObject[key];
// determine if that was an op
if (
keyUpdate &&
typeof keyUpdate === 'object' &&
keyUpdate.__op &&
['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1
) {
// only valid ops that produce an actionable result
// the op may have happend on a keypath
expandResultOnKeyPath(response, key, result);
}
});
return Promise.resolve(response);
}
function joinTableName(className, key) {
return `_Join:${key}:${className}`;
}
@@ -397,8 +364,9 @@ class DatabaseController {
schemaCache: any;
schemaPromise: ?Promise<SchemaController.SchemaController>;
_transactionalSession: ?any;
options: ParseServerOptions;
constructor(adapter: StorageAdapter, schemaCache: any) {
constructor(adapter: StorageAdapter, schemaCache: any, options: ParseServerOptions) {
this.adapter = adapter;
this.schemaCache = schemaCache;
// We don't want a mutable this.schema, because then you could have
@@ -406,6 +374,7 @@ class DatabaseController {
// it. Instead, use loadSchema to get a schema.
this.schemaPromise = null;
this._transactionalSession = null;
this.options = options;
}
collectionExists(className: string): Promise<boolean> {
@@ -644,7 +613,7 @@ class DatabaseController {
if (skipSanitization) {
return Promise.resolve(result);
}
return sanitizeDatabaseResult(originalUpdate, result);
return this._sanitizeDatabaseResult(originalUpdate, result);
});
});
}
@@ -871,7 +840,7 @@ class DatabaseController {
object,
relationUpdates
).then(() => {
return sanitizeDatabaseResult(originalObject, result.ops[0]);
return this._sanitizeDatabaseResult(originalObject, result.ops[0]);
});
});
});
@@ -1717,6 +1686,60 @@ class DatabaseController {
]);
}
_expandResultOnKeyPath(object: any, key: string, value: any): any {
if (key.indexOf('.') < 0) {
object[key] = value[key];
return object;
}
const path = key.split('.');
const firstKey = path[0];
const nextPath = path.slice(1).join('.');
// Scan request data for denied keywords
if (this.options && this.options.requestKeywordDenylist) {
// Scan request data for denied keywords
for (const keyword of this.options.requestKeywordDenylist) {
const match = Utils.objectContainsKeyValue({ firstKey: undefined }, keyword.key, undefined);
if (match) {
throw new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
);
}
}
}
object[firstKey] = this._expandResultOnKeyPath(
object[firstKey] || {},
nextPath,
value[firstKey]
);
delete object[key];
return object;
}
_sanitizeDatabaseResult(originalObject: any, result: any): Promise<any> {
const response = {};
if (!result) {
return Promise.resolve(response);
}
Object.keys(originalObject).forEach(key => {
const keyUpdate = originalObject[key];
// determine if that was an op
if (
keyUpdate &&
typeof keyUpdate === 'object' &&
keyUpdate.__op &&
['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1
) {
// only valid ops that produce an actionable result
// the op may have happened on a keypath
this._expandResultOnKeyPath(response, key, result);
}
});
return Promise.resolve(response);
}
static _validateQuery: any => void;
}
+2 -1
View File
@@ -167,7 +167,8 @@ export function getDatabaseController(
}
return new DatabaseController(
databaseAdapter,
new SchemaCache(cacheController, schemaCacheTTL, enableSingleSchemaCache)
new SchemaCache(cacheController, schemaCacheTTL, enableSingleSchemaCache),
options
);
}
+18
View File
@@ -179,6 +179,14 @@ class ParseLiveQueryServer {
deletedParseObject = res.object.toJSON();
deletedParseObject.className = className;
}
if (
(deletedParseObject.className === '_User' ||
deletedParseObject.className === '_Session') &&
!client.hasMasterKey
) {
delete deletedParseObject.sessionToken;
delete deletedParseObject.authData;
}
client.pushDelete(requestId, deletedParseObject);
})
.catch(error => {
@@ -315,6 +323,16 @@ class ParseLiveQueryServer {
originalParseObject = res.original.toJSON();
originalParseObject.className = res.original.className || className;
}
if (
(currentParseObject.className === '_User' ||
currentParseObject.className === '_Session') &&
!client.hasMasterKey
) {
delete currentParseObject.sessionToken;
delete originalParseObject?.sessionToken;
delete currentParseObject.authData;
delete originalParseObject?.authData;
}
const functionName =
'push' + message.event.charAt(0).toUpperCase() + message.event.slice(1);
if (client[functionName]) {
+2 -1
View File
@@ -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
+18
View File
@@ -336,6 +336,24 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY',
help: 'Read-only key, which has the same capabilities as MasterKey without writes',
},
requestKeywordDenylist: {
env: 'PARSE_SERVER_REQUEST_KEYWORD_DENYLIST',
help:
'An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns.',
action: parsers.arrayParser,
default: [
{
key: '_bsontype',
value: 'Code',
},
{
key: 'constructor',
},
{
key: '__proto__',
},
],
},
restAPIKey: {
env: 'PARSE_SERVER_REST_API_KEY',
help: 'Key for REST calls',
+1
View File
@@ -62,6 +62,7 @@
* @property {String} publicServerURL Public URL to your parse server with http:// or https://.
* @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications
* @property {String} readOnlyMasterKey Read-only key, which has the same capabilities as MasterKey without writes
* @property {RequestKeywordDenylist[]} requestKeywordDenylist An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns.
* @property {String} restAPIKey Key for REST calls
* @property {Boolean} revokeSessionOnPasswordReset When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.
* @property {Boolean} scheduledPush Configuration for push scheduling, defaults to false.
+7
View File
@@ -12,6 +12,10 @@ type Adapter<T> = string | any | T;
type NumberOrBoolean = number | boolean;
type NumberOrString = number | string;
type ProtectedFields = any;
type RequestKeywordDenylist = {
key: string | any,
value: any,
};
export interface ParseServerOptions {
/* Your Parse Application ID
@@ -220,6 +224,9 @@ export interface ParseServerOptions {
serverStartComplete: ?(error: ?Error) => void;
/* Callback when server has closed */
serverCloseComplete: ?() => void;
/* An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns.
:DEFAULT: [{"key":"_bsontype","value":"Code"},{"key":"constructor"},{"key":"__proto__"}] */
requestKeywordDenylist: ?(RequestKeywordDenylist[]);
}
export interface CustomPagesOptions {
+1 -1
View File
@@ -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);
}
+18
View File
@@ -6,6 +6,7 @@ var SchemaController = require('./Controllers/SchemaController');
var deepcopy = require('deepcopy');
const Auth = require('./Auth');
const Utils = require('./Utils');
var cryptoUtils = require('./cryptoUtils');
var passwordCrypto = require('./password');
var Parse = require('parse/node');
@@ -61,6 +62,19 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
}
}
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)}.`
);
}
}
}
// When the operation is complete, this.response may have several
// fields.
// response: the actual data to be returned
@@ -857,6 +871,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: {
+38
View File
@@ -0,0 +1,38 @@
/**
* utils.js
* @file General purpose utilities
* @description General purpose utilities.
*/
/**
* The general purpose utilities.
*/
class Utils {
/**
* Deep-scans an object for a matching key/value definition.
* @param {Object} obj The object to scan.
* @param {String | undefined} key The key to match, or undefined if only the value should be matched.
* @param {any | undefined} value The value to match, or undefined if only the key should be matched.
* @returns {Boolean} True if a match was found, false otherwise.
*/
static objectContainsKeyValue(obj, key, value) {
const isMatch = (a, b) => (typeof a === 'string' && new RegExp(b).test(a)) || a === b;
const isKeyMatch = k => isMatch(k, key);
const isValueMatch = v => isMatch(v, value);
for (const [k, v] of Object.entries(obj)) {
if (key !== undefined && value === undefined && isKeyMatch(k)) {
return true;
} else if (key === undefined && value !== undefined && isValueMatch(v)) {
return true;
} else if (key !== undefined && value !== undefined && isKeyMatch(k) && isValueMatch(v)) {
return true;
}
if (['[object Object]', '[object Array]'].includes(Object.prototype.toString.call(v))) {
return Utils.objectContainsKeyValue(v, key, value);
}
}
return false;
}
}
module.exports = Utils;