mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
30
Commits
6.4.0-alpha.3
...
4.10.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c844772ea | ||
|
|
886bfd7cac | ||
|
|
318c20319a | ||
|
|
6f25ea93fb | ||
|
|
cd41626625 | ||
|
|
350ecdee59 | ||
|
|
b465f7bade | ||
|
|
d7f2bb49d4 | ||
|
|
4bd13088e1 | ||
|
|
611332ea33 | ||
|
|
65c13febf5 | ||
|
|
adf7d928de | ||
|
|
065facdc53 | ||
|
|
4ac4b7f710 | ||
|
|
6683cd96b6 | ||
|
|
ef2ec21355 | ||
|
|
c71ae7d478 | ||
|
|
6ae5835b19 | ||
|
|
0bfa6b7cc1 | ||
|
|
0be0b87b2d | ||
|
|
f3133acf21 | ||
|
|
7e1da90bb2 | ||
|
|
0e3cae5c67 | ||
|
|
f0d5232443 | ||
|
|
a3483d82c8 | ||
|
|
3c42584f59 | ||
|
|
97b1dcad3b | ||
|
|
3a5c38d2e4 | ||
|
|
93d643e3eb | ||
|
|
1306da7454 |
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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}}
|
||||
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
@@ -0,0 +1,14 @@
|
||||
{{> header}}
|
||||
|
||||
{{#each commitGroups}}
|
||||
|
||||
{{#if title}}
|
||||
### {{title}}
|
||||
|
||||
{{/if}}
|
||||
{{#each commits}}
|
||||
{{> commit root=@root}}
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
|
||||
{{> footer}}
|
||||
+70
-4
@@ -1,9 +1,75 @@
|
||||
## Parse Server Changelog
|
||||
## [4.10.7](https://github.com/parse-community/parse-server/compare/4.10.6...4.10.7) (2022-03-11)
|
||||
|
||||
### master
|
||||
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.5.0...master)
|
||||
|
||||
### 4.5.0
|
||||
### 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))
|
||||
|
||||
## [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
@@ -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
|
||||
|
||||
Generated
+5250
-661
File diff suppressed because it is too large
Load Diff
+25
-17
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "4.5.0",
|
||||
"version": "4.10.7",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -24,8 +24,8 @@
|
||||
"@graphql-tools/stitch": "6.2.4",
|
||||
"@graphql-tools/utils": "6.2.4",
|
||||
"@parse/fs-files-adapter": "1.2.0",
|
||||
"@parse/push-adapter": "3.4.0",
|
||||
"@parse/s3-files-adapter": "1.6.0",
|
||||
"@parse/push-adapter": "3.4.1",
|
||||
"@parse/s3-files-adapter": "1.6.2",
|
||||
"@parse/simple-mailgun-adapter": "1.1.0",
|
||||
"apollo-server-express": "2.19.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
+15
-7
@@ -87,13 +87,17 @@ describe('OAuth', function () {
|
||||
done();
|
||||
}
|
||||
|
||||
it('Should fail a GET request', done => {
|
||||
it('GET request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
/*
|
||||
This endpoint has been chosen to make a request to an endpoint that requires OAuth which fails due to missing authentication.
|
||||
Any other endpoint from the Twitter API that requires OAuth can be used instead in case the currently used endpoint deprecates.
|
||||
*/
|
||||
const options = {
|
||||
host: 'api.twitter.com',
|
||||
consumer_key: 'XXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
consumer_secret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
consumer_key: 'invalid_consumer_key',
|
||||
consumer_secret: 'invalid_consumer_secret',
|
||||
};
|
||||
const path = '/1.1/help/configuration.json';
|
||||
const path = '/1.1/favorites/list.json';
|
||||
const params = { lang: 'en' };
|
||||
const oauthClient = new OAuth(options);
|
||||
oauthClient.get(path, params).then(function (data) {
|
||||
@@ -101,11 +105,15 @@ describe('OAuth', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail a POST request', done => {
|
||||
it('POST request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
/*
|
||||
This endpoint has been chosen to make a request to an endpoint that requires OAuth which fails due to missing authentication.
|
||||
Any other endpoint from the Twitter API that requires OAuth can be used instead in case the currently used endpoint deprecates.
|
||||
*/
|
||||
const options = {
|
||||
host: 'api.twitter.com',
|
||||
consumer_key: 'XXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
consumer_secret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
consumer_key: 'invalid_consumer_key',
|
||||
consumer_secret: 'invalid_consumer_secret',
|
||||
};
|
||||
const body = {
|
||||
lang: 'en',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,283 @@
|
||||
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]*"}.');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import deepcopy from 'deepcopy';
|
||||
import logger from '../logger';
|
||||
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 +258,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 +363,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 +373,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 +612,7 @@ class DatabaseController {
|
||||
if (skipSanitization) {
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
return sanitizeDatabaseResult(originalUpdate, result);
|
||||
return this._sanitizeDatabaseResult(originalUpdate, result);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -871,7 +839,7 @@ class DatabaseController {
|
||||
object,
|
||||
relationUpdates
|
||||
).then(() => {
|
||||
return sanitizeDatabaseResult(originalObject, result.ops[0]);
|
||||
return this._sanitizeDatabaseResult(originalObject, result.ops[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1717,6 +1685,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 isMatch = (a, b) => (typeof a === 'string' && new RegExp(a).test(b)) || a === b;
|
||||
if (isMatch(firstKey, keyword.key)) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -167,7 +167,8 @@ export function getDatabaseController(
|
||||
}
|
||||
return new DatabaseController(
|
||||
databaseAdapter,
|
||||
new SchemaCache(cacheController, schemaCacheTTL, enableSingleSchemaCache)
|
||||
new SchemaCache(cacheController, schemaCacheTTL, enableSingleSchemaCache),
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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(a).test(b)) || a === b;
|
||||
const isKeyMatch = k => isMatch(key, k);
|
||||
const isValueMatch = v => isMatch(value, v);
|
||||
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;
|
||||
Reference in New Issue
Block a user