Skip to content

FileUpload options for Server Config #7071

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Dec 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

### master
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.5.0...master)

__BREAKING CHANGES:__
- NEW: Added file upload restriction. File upload is now only allowed for authenticated users by default for improved security. To allow file upload also for Anonymous Users or Public, set the `fileUpload` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html). [#7071](https://github.com/parse-community/parse-server/pull/7071). Thanks to [dblythy](https://github.com/dblythy).
___
- IMPROVE: Optimize queries on classes with pointer permissions. [#7061](https://github.com/parse-community/parse-server/pull/7061). Thanks to [Pedro Diaz](https://github.com/pdiaz)

### 4.5.0
Expand Down
13 changes: 4 additions & 9 deletions resources/buildConfigDefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ function getENVPrefix(iface) {
'LiveQueryOptions' : 'PARSE_SERVER_LIVEQUERY_',
'IdempotencyOptions' : 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
'AccountLockoutOptions' : 'PARSE_SERVER_ACCOUNT_LOCKOUT_',
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_'
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_',
'FileUploadOptions' : 'PARSE_SERVER_FILE_UPLOAD_'
}
if (options[iface.id.name]) {
return options[iface.id.name]
Expand Down Expand Up @@ -163,14 +164,8 @@ function parseDefaultValue(elt, value, t) {
if (type == 'NumberOrBoolean') {
literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value));
}
if (type == 'CustomPagesOptions') {
const object = parsers.objectParser(value);
const props = Object.keys(object).map((key) => {
return t.objectProperty(key, object[value]);
});
literalValue = t.objectExpression(props);
}
if (type == 'IdempotencyOptions') {
const literalTypes = ['IdempotencyOptions','FileUploadOptions','CustomPagesOptions'];
if (literalTypes.includes(type)) {
const object = parsers.objectParser(value);
const props = Object.keys(object).map((key) => {
return t.objectProperty(key, object[value]);
Expand Down
193 changes: 193 additions & 0 deletions spec/ParseFile.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
'use strict';

const request = require('../lib/request');
const Definitions = require('../src/Options/Definitions');

const str = 'Hello World!';
const data = [];
Expand Down Expand Up @@ -860,4 +861,196 @@ describe('Parse.File testing', () => {
});
});
});

describe('file upload configuration', () => {
it('allows file upload only for authenticated user by default', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: Definitions.FileUploadOptions.enableForPublic.default,
enableForAnonymousUser: Definitions.FileUploadOptions.enableForAnonymousUser.default,
enableForAuthenticatedUser: Definitions.FileUploadOptions.enableForAuthenticatedUser.default,
}
});
let file = new Parse.File('hello.txt', data, 'text/plain');
await expectAsync(file.save()).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by public is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const anonUser = await Parse.AnonymousUtils.logIn();
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by anonymous user is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const authUser = await Parse.User.signUp('user', 'password');
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeResolved();
});

it('allows file upload with master key', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: false,
enableForAnonymousUser: false,
enableForAuthenticatedUser: false,
},
});
const file = new Parse.File('hello.txt', data, 'text/plain');
await expectAsync(file.save({ useMasterKey: true })).toBeResolved();
});

it('rejects all file uploads', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: false,
enableForAnonymousUser: false,
enableForAuthenticatedUser: false,
},
});
let file = new Parse.File('hello.txt', data, 'text/plain');
await expectAsync(file.save()).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by public is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const anonUser = await Parse.AnonymousUtils.logIn();
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by anonymous user is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const authUser = await Parse.User.signUp('user', 'password');
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by authenticated user is disabled.')
);
});

it('allows all file uploads', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
enableForAnonymousUser: true,
enableForAuthenticatedUser: true,
},
});
let file = new Parse.File('hello.txt', data, 'text/plain');
await expectAsync(file.save()).toBeResolved();
file = new Parse.File('hello.txt', data, 'text/plain');
const anonUser = await Parse.AnonymousUtils.logIn();
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeResolved();
file = new Parse.File('hello.txt', data, 'text/plain');
const authUser = await Parse.User.signUp('user', 'password');
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeResolved();
});

it('allows file upload only for public', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
enableForAnonymousUser: false,
enableForAuthenticatedUser: false,
},
});
let file = new Parse.File('hello.txt', data, 'text/plain');
await expectAsync(file.save()).toBeResolved();
file = new Parse.File('hello.txt', data, 'text/plain');
const anonUser = await Parse.AnonymousUtils.logIn();
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by anonymous user is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const authUser = await Parse.User.signUp('user', 'password');
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by authenticated user is disabled.')
);
});

it('allows file upload only for anonymous user', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: false,
enableForAnonymousUser: true,
enableForAuthenticatedUser: false,
},
});
let file = new Parse.File('hello.txt', data, 'text/plain');
await expectAsync(file.save()).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by public is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const anonUser = await Parse.AnonymousUtils.logIn();
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeResolved();
file = new Parse.File('hello.txt', data, 'text/plain');
const authUser = await Parse.User.signUp('user', 'password');
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by authenticated user is disabled.')
);
});

it('allows file upload only for authenticated user', async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: false,
enableForAnonymousUser: false,
enableForAuthenticatedUser: true,
},
});
let file = new Parse.File('hello.txt', data, 'text/plain');
await expectAsync(file.save()).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by public is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const anonUser = await Parse.AnonymousUtils.logIn();
await expectAsync(file.save({ sessionToken: anonUser.getSessionToken() })).toBeRejectedWith(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'File upload by anonymous user is disabled.')
);
file = new Parse.File('hello.txt', data, 'text/plain');
const authUser = await Parse.User.signUp('user', 'password');
await expectAsync(file.save({ sessionToken: authUser.getSessionToken() })).toBeResolved();
});

it('rejects invalid fileUpload configuration', async () => {
const invalidConfigs = [
{ fileUpload: [] },
{ fileUpload: 1 },
{ fileUpload: "string" },
];
const validConfigs = [
{ fileUpload: {} },
{ fileUpload: null },
{ fileUpload: undefined },
];
const keys = [
"enableForPublic",
"enableForAnonymousUser",
"enableForAuthenticatedUser",
];
const invalidValues = [
[],
{},
1,
"string",
null,
];
const validValues = [
undefined,
true,
false,
];
for (const config of invalidConfigs) {
await expectAsync(reconfigureServer(config)).toBeRejectedWith(
'fileUpload must be an object value.'
);
}
for (const config of validConfigs) {
await expectAsync(reconfigureServer(config)).toBeResolved();
}
for (const key of keys) {
for (const value of invalidValues) {
await expectAsync(reconfigureServer({ fileUpload: { [key]: value }})).toBeRejectedWith(
`fileUpload.${key} must be a boolean value.`
);
}
for (const value of validValues) {
await expectAsync(reconfigureServer({ fileUpload: { [key]: value }})).toBeResolved();
}
}
});
});
});
5 changes: 5 additions & 0 deletions spec/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ const defaultConfiguration = {
fileKey: 'test',
silent,
logLevel,
fileUpload: {
enableForPublic: true,
enableForAnonymousUser: true,
enableForAuthenticatedUser: true,
},
push: {
android: {
senderId: 'yolo',
Expand Down
32 changes: 30 additions & 2 deletions src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import AppCache from './cache';
import SchemaCache from './Controllers/SchemaCache';
import DatabaseController from './Controllers/DatabaseController';
import net from 'net';
import { IdempotencyOptions } from './Options/Definitions';
import {
IdempotencyOptions,
FileUploadOptions,
} from './Options/Definitions';

function removeTrailingSlash(str) {
if (!str) {
Expand Down Expand Up @@ -71,6 +74,7 @@ export class Config {
allowHeaders,
idempotencyOptions,
emailVerifyTokenReuseIfValid,
fileUpload,
}) {
if (masterKey === readOnlyMasterKey) {
throw new Error('masterKey and readOnlyMasterKey should be different');
Expand All @@ -88,8 +92,8 @@ export class Config {
}

this.validateAccountLockoutPolicy(accountLockout);

this.validatePasswordPolicy(passwordPolicy);
this.validateFileUploadOptions(fileUpload);

if (typeof revokeSessionOnPasswordReset !== 'boolean') {
throw 'revokeSessionOnPasswordReset must be a boolean value';
Expand Down Expand Up @@ -245,6 +249,30 @@ export class Config {
}
}

static validateFileUploadOptions(fileUpload) {
if (!fileUpload) {
fileUpload = {};
}
if (typeof fileUpload !== 'object' || fileUpload instanceof Array) {
throw 'fileUpload must be an object value.';
}
if (fileUpload.enableForAnonymousUser === undefined) {
fileUpload.enableForAnonymousUser = FileUploadOptions.enableForAnonymousUser.default;
} else if (typeof fileUpload.enableForAnonymousUser !== 'boolean') {
throw 'fileUpload.enableForAnonymousUser must be a boolean value.';
}
if (fileUpload.enableForPublic === undefined) {
fileUpload.enableForPublic = FileUploadOptions.enableForPublic.default;
} else if (typeof fileUpload.enableForPublic !== 'boolean') {
throw 'fileUpload.enableForPublic must be a boolean value.';
}
if (fileUpload.enableForAuthenticatedUser === undefined) {
fileUpload.enableForAuthenticatedUser = FileUploadOptions.enableForAuthenticatedUser.default;
} else if (typeof fileUpload.enableForAuthenticatedUser !== 'boolean') {
throw 'fileUpload.enableForAuthenticatedUser must be a boolean value.';
}
}

static validateMasterKeyIps(masterKeyIps) {
for (const ip of masterKeyIps) {
if (!net.isIP(ip)) {
Expand Down
Loading