Skip to content

Commit 16a9d0a

Browse files
committed
feat: add parse server option fileUpload.fileTypes
1 parent 07acecd commit 16a9d0a

File tree

9 files changed

+189
-2
lines changed

9 files changed

+189
-2
lines changed

spec/.eslintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"jequal": true,
3535
"create": true,
3636
"arrayContains": true,
37-
"databaseAdapter": true
37+
"databaseAdapter": true,
38+
"requestWithExpectedError": true
3839
},
3940
"rules": {
4041
"no-console": [0],

spec/ParseFile.spec.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,150 @@ describe('Parse.File testing', () => {
11091109
await expectAsync(reconfigureServer({ fileUpload: { [key]: value } })).toBeResolved();
11101110
}
11111111
}
1112+
await expectAsync(
1113+
reconfigureServer({
1114+
fileUpload: {
1115+
fileTypes: 1,
1116+
},
1117+
})
1118+
).toBeRejectedWith('fileUpload.fileTypes must be an array or string.');
1119+
});
1120+
});
1121+
describe('fileTypes', () => {
1122+
it('works with _ContentType', async () => {
1123+
await reconfigureServer({
1124+
fileUpload: {
1125+
enableForPublic: true,
1126+
fileTypes: '(^image)(/)[a-zA-Z0-9_]*',
1127+
},
1128+
});
1129+
await expectAsync(
1130+
requestWithExpectedError({
1131+
method: 'POST',
1132+
url: 'http://localhost:8378/1/files/file',
1133+
body: JSON.stringify({
1134+
_ApplicationId: 'test',
1135+
_JavaScriptKey: 'test',
1136+
_ContentType: 'text/html',
1137+
base64: 'PGh0bWw+PC9odG1sPgo=',
1138+
}),
1139+
})
1140+
).toBeRejectedWith(
1141+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of type text/html is disabled.`)
1142+
);
1143+
});
1144+
1145+
it('works without Content-Type', async () => {
1146+
await reconfigureServer({
1147+
fileUpload: {
1148+
enableForPublic: true,
1149+
fileTypes: '(^image)(/)[a-zA-Z0-9_]*',
1150+
},
1151+
});
1152+
const headers = {
1153+
'X-Parse-Application-Id': 'test',
1154+
'X-Parse-REST-API-Key': 'rest',
1155+
};
1156+
await expectAsync(
1157+
requestWithExpectedError({
1158+
method: 'POST',
1159+
headers: headers,
1160+
url: 'http://localhost:8378/1/files/file.html',
1161+
body: '<html></html>\n',
1162+
})
1163+
).toBeRejectedWith(
1164+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of type text/html is disabled.`)
1165+
);
1166+
});
1167+
1168+
it('works with array', async () => {
1169+
await reconfigureServer({
1170+
fileUpload: {
1171+
enableForPublic: true,
1172+
fileTypes: ['image/jpg'],
1173+
},
1174+
});
1175+
await expectAsync(
1176+
requestWithExpectedError({
1177+
method: 'POST',
1178+
url: 'http://localhost:8378/1/files/file',
1179+
body: JSON.stringify({
1180+
_ApplicationId: 'test',
1181+
_JavaScriptKey: 'test',
1182+
_ContentType: 'text/html',
1183+
base64: 'PGh0bWw+PC9odG1sPgo=',
1184+
}),
1185+
})
1186+
).toBeRejectedWith(
1187+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of type text/html is disabled.`)
1188+
);
1189+
});
1190+
1191+
it('works with array without Content-Type', async () => {
1192+
await reconfigureServer({
1193+
fileUpload: {
1194+
enableForPublic: true,
1195+
fileTypes: ['image/jpg'],
1196+
},
1197+
});
1198+
const headers = {
1199+
'X-Parse-Application-Id': 'test',
1200+
'X-Parse-REST-API-Key': 'rest',
1201+
};
1202+
await expectAsync(
1203+
requestWithExpectedError({
1204+
method: 'POST',
1205+
headers: headers,
1206+
url: 'http://localhost:8378/1/files/file.html',
1207+
body: '<html></html>\n',
1208+
})
1209+
).toBeRejectedWith(
1210+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of type text/html is disabled.`)
1211+
);
1212+
});
1213+
1214+
it('works with array with correct file type', async () => {
1215+
await reconfigureServer({
1216+
fileUpload: {
1217+
enableForPublic: true,
1218+
fileTypes: ['text/html'],
1219+
},
1220+
});
1221+
const response = await request({
1222+
method: 'POST',
1223+
url: 'http://localhost:8378/1/files/file',
1224+
body: JSON.stringify({
1225+
_ApplicationId: 'test',
1226+
_JavaScriptKey: 'test',
1227+
_ContentType: 'text/html',
1228+
base64: 'PGh0bWw+PC9odG1sPgo=',
1229+
}),
1230+
});
1231+
const b = response.data;
1232+
expect(b.name).toMatch(/_file.html$/);
1233+
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
1234+
});
1235+
1236+
it('works with regex with correct file type', async () => {
1237+
await reconfigureServer({
1238+
fileUpload: {
1239+
enableForPublic: true,
1240+
fileTypes: '(^text)(/)[a-zA-Z0-9_]*',
1241+
},
1242+
});
1243+
const headers = {
1244+
'X-Parse-Application-Id': 'test',
1245+
'X-Parse-REST-API-Key': 'rest',
1246+
};
1247+
const response = await request({
1248+
method: 'POST',
1249+
headers: headers,
1250+
url: 'http://localhost:8378/1/files/file.html',
1251+
body: '<html></html>\n',
1252+
});
1253+
const b = response.data;
1254+
expect(b.name).toMatch(/_file.html$/);
1255+
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
11121256
});
11131257
});
11141258
});

spec/helper.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const semver = require('semver');
44
const CurrentSpecReporter = require('./support/CurrentSpecReporter.js');
55
const { SpecReporter } = require('jasmine-spec-reporter');
66
const SchemaCache = require('../lib/Adapters/Cache/SchemaCache').default;
7+
const request = require('../lib/request');
78

89
// Ensure localhost resolves to ipv4 address first on node v17+
910
if (dns.setDefaultResultOrder) {
@@ -30,6 +31,13 @@ if (global._babelPolyfill) {
3031
console.error('We should not use polyfilled tests');
3132
process.exit(1);
3233
}
34+
global.requestWithExpectedError = async params => {
35+
try {
36+
return await request(params);
37+
} catch (e) {
38+
throw new Error(e.data.error);
39+
}
40+
};
3341
process.noDeprecation = true;
3442

3543
const cache = require('../lib/cache').default;

src/Config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,11 @@ export class Config {
421421
} else if (typeof fileUpload.enableForAuthenticatedUser !== 'boolean') {
422422
throw 'fileUpload.enableForAuthenticatedUser must be a boolean value.';
423423
}
424+
if (fileUpload.fileTypes === undefined) {
425+
fileUpload.fileTypes = FileUploadOptions.fileTypes.default;
426+
} else if (typeof fileUpload.fileTypes !== 'string' && !Array.isArray(fileUpload.fileTypes)) {
427+
throw 'fileUpload.fileTypes must be an array or string.';
428+
}
424429
}
425430

426431
static validateMasterKeyIps(masterKeyIps) {

src/Deprecator/Deprecations.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ module.exports = [
2424
},
2525
{ optionKey: 'enforcePrivateUsers', changeNewDefault: 'true' },
2626
{ optionKey: 'allowClientClassCreation', changeNewDefault: 'false' },
27+
{ optionKey: 'fileUpload.fileTypes', changeNewDefault: '^(.(?!.*html?))*$' },
2728
];

src/Options/Definitions.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,11 @@ module.exports.FileUploadOptions = {
866866
action: parsers.booleanParser,
867867
default: false,
868868
},
869+
fileTypes: {
870+
env: 'PARSE_SERVER_FILE_UPLOAD_FILE_TYPES',
871+
help: 'If set, allowed content types of files',
872+
default: '.*',
873+
},
869874
};
870875
module.exports.DatabaseOptions = {
871876
enableSchemaHooks: {

src/Options/docs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@
202202
* @property {Boolean} enableForAnonymousUser Is true if file upload should be allowed for anonymous users.
203203
* @property {Boolean} enableForAuthenticatedUser Is true if file upload should be allowed for authenticated users.
204204
* @property {Boolean} enableForPublic Is true if file upload should be allowed for anyone, regardless of user authentication.
205+
* @property {String} fileTypes If set, allowed content types of files
205206
*/
206207

207208
/**

src/Options/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,9 @@ export interface PasswordPolicyOptions {
490490
}
491491

492492
export interface FileUploadOptions {
493+
/* If set, allowed content types of files
494+
:DEFAULT: .* */
495+
fileTypes: ?string;
493496
/* Is true if file upload should be allowed for anonymous users.
494497
:DEFAULT: false */
495498
enableForAnonymousUser: ?boolean;

src/Routers/FilesRouter.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export class FilesRouter {
124124
}
125125
const filesController = config.filesController;
126126
const { filename } = req.params;
127-
const contentType = req.get('Content-type');
127+
const contentType = req.get('Content-type') || mime.getType(filename);
128128

129129
if (!req.body || !req.body.length) {
130130
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.'));
@@ -137,6 +137,25 @@ export class FilesRouter {
137137
return;
138138
}
139139

140+
const fileTypes = config.fileUpload && config.fileUpload.fileTypes;
141+
if (!isMaster && fileTypes) {
142+
try {
143+
if (Array.isArray(fileTypes)) {
144+
if (!fileTypes.includes(contentType)) {
145+
throw `File upload of type ${contentType} is disabled.`;
146+
}
147+
} else if (typeof fileTypes === 'string') {
148+
const regex = new RegExp(fileTypes);
149+
if (!regex.test(contentType)) {
150+
throw `File upload of type ${contentType} is disabled.`;
151+
}
152+
}
153+
} catch (e) {
154+
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, e));
155+
return;
156+
}
157+
}
158+
140159
const base64 = req.body.toString('base64');
141160
const file = new Parse.File(filename, { base64 }, contentType);
142161
const { metadata = {}, tags = {} } = req.fileData || {};

0 commit comments

Comments
 (0)