Skip to content

Commit 70af738

Browse files
committed
add file ext
1 parent 32c29e5 commit 70af738

File tree

7 files changed

+74
-50
lines changed

7 files changed

+74
-50
lines changed

spec/ParseFile.spec.js

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,14 @@ describe('Parse.File testing', () => {
3737
});
3838
});
3939

40-
it('works with _ContentType', done => {
41-
request({
40+
it('works with _ContentType', async () => {
41+
await reconfigureServer({
42+
fileUpload: {
43+
enableForPublic: true,
44+
fileExtensions: '*',
45+
},
46+
});
47+
let response = await request({
4248
method: 'POST',
4349
url: 'http://localhost:8378/1/files/file',
4450
body: JSON.stringify({
@@ -47,21 +53,18 @@ describe('Parse.File testing', () => {
4753
_ContentType: 'text/html',
4854
base64: 'PGh0bWw+PC9odG1sPgo=',
4955
}),
50-
}).then(response => {
51-
const b = response.data;
52-
expect(b.name).toMatch(/_file.html/);
53-
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
54-
request({ url: b.url }).then(response => {
55-
const body = response.text;
56-
try {
57-
expect(response.headers['content-type']).toMatch('^text/html');
58-
expect(body).toEqual('<html></html>\n');
59-
} catch (e) {
60-
jfail(e);
61-
}
62-
done();
63-
});
6456
});
57+
const b = response.data;
58+
expect(b.name).toMatch(/_file.html/);
59+
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
60+
response = await request({ url: b.url });
61+
const body = response.text;
62+
try {
63+
expect(response.headers['content-type']).toMatch('^text/html');
64+
expect(body).toEqual('<html></html>\n');
65+
} catch (e) {
66+
jfail(e);
67+
}
6568
});
6669

6770
it('works without Content-Type', done => {
@@ -351,25 +354,28 @@ describe('Parse.File testing', () => {
351354
ok(object.toJSON().file.url);
352355
});
353356

354-
it('content-type used with no extension', done => {
357+
it('content-type used with no extension', async () => {
358+
await reconfigureServer({
359+
fileUpload: {
360+
enableForPublic: true,
361+
fileExtensions: '*',
362+
},
363+
});
355364
const headers = {
356365
'Content-Type': 'text/html',
357366
'X-Parse-Application-Id': 'test',
358367
'X-Parse-REST-API-Key': 'rest',
359368
};
360-
request({
369+
let response = await request({
361370
method: 'POST',
362371
headers: headers,
363372
url: 'http://localhost:8378/1/files/file',
364373
body: 'fee fi fo',
365-
}).then(response => {
366-
const b = response.data;
367-
expect(b.name).toMatch(/\.html$/);
368-
request({ url: b.url }).then(response => {
369-
expect(response.headers['content-type']).toMatch(/^text\/html/);
370-
done();
371-
});
372374
});
375+
const b = response.data;
376+
expect(b.name).toMatch(/\.html$/);
377+
response = await request({ url: b.url });
378+
expect(response.headers['content-type']).toMatch(/^text\/html/);
373379
});
374380

375381
it('filename is url encoded', done => {
@@ -1304,12 +1310,13 @@ describe('Parse.File testing', () => {
13041310
fileExtensions: 1,
13051311
},
13061312
})
1307-
).toBeRejectedWith('fileUpload.fileExtensions must be an array.');
1313+
).toBeRejectedWith('fileUpload.fileExtensions must be an array or string.');
13081314
});
13091315
});
13101316
describe('fileExtensions', () => {
13111317
it('works with _ContentType', async () => {
13121318
await reconfigureServer({
1319+
silent: false,
13131320
fileUpload: {
13141321
enableForPublic: true,
13151322
fileExtensions: ['png'],
@@ -1329,7 +1336,7 @@ describe('Parse.File testing', () => {
13291336
throw new Error(e.data.error);
13301337
})
13311338
).toBeRejectedWith(
1332-
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of type html is disabled.`)
1339+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
13331340
);
13341341
});
13351342

@@ -1353,7 +1360,7 @@ describe('Parse.File testing', () => {
13531360
throw new Error(e.data.error);
13541361
})
13551362
).toBeRejectedWith(
1356-
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of type html is disabled.`)
1363+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
13571364
);
13581365
});
13591366

@@ -1378,7 +1385,7 @@ describe('Parse.File testing', () => {
13781385
throw new Error(e.data.error);
13791386
})
13801387
).toBeRejectedWith(
1381-
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of type html is disabled.`)
1388+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
13821389
);
13831390
});
13841391

@@ -1403,7 +1410,7 @@ describe('Parse.File testing', () => {
14031410
throw new Error(e.data.error);
14041411
})
14051412
).toBeRejectedWith(
1406-
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of type html is disabled.`)
1413+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
14071414
);
14081415
});
14091416

spec/helper.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ const defaultConfiguration = {
110110
enableForPublic: true,
111111
enableForAnonymousUser: true,
112112
enableForAuthenticatedUser: true,
113-
fileExtensions: ['*']
114113
},
115114
push: {
116115
android: {

src/Config.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,8 +426,11 @@ export class Config {
426426
}
427427
if (fileUpload.fileExtensions === undefined) {
428428
fileUpload.fileExtensions = FileUploadOptions.fileExtensions.default;
429-
} else if (!Array.isArray(fileUpload.fileExtensions)) {
430-
throw 'fileUpload.fileExtensions must be an array.';
429+
} else if (
430+
!Array.isArray(fileUpload.fileExtensions) &&
431+
typeof fileUpload.fileExtensions !== 'string'
432+
) {
433+
throw 'fileUpload.fileExtensions must be an array or string.';
431434
}
432435
}
433436

src/Options/Definitions.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -875,9 +875,9 @@ module.exports.FileUploadOptions = {
875875
},
876876
fileExtensions: {
877877
env: 'PARSE_SERVER_FILE_UPLOAD_FILE_EXTENSIONS',
878-
help: 'Allowed content types of files',
879-
action: parsers.arrayParser,
880-
default: [],
878+
help:
879+
"Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.<br><br>It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.<br><br>Defaults to `^[^hH][^tT][^mM][^lL]?$` which allows any file extension except HTML files.",
880+
default: '^[^hH][^tT][^mM][^lL]?$',
881881
},
882882
};
883883
module.exports.DatabaseOptions = {

src/Options/docs.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@
203203
* @property {Boolean} enableForAnonymousUser Is true if file upload should be allowed for anonymous users.
204204
* @property {Boolean} enableForAuthenticatedUser Is true if file upload should be allowed for authenticated users.
205205
* @property {Boolean} enableForPublic Is true if file upload should be allowed for anyone, regardless of user authentication.
206-
* @property {String[]} fileExtensions Allowed content types of files
206+
* @property {String} fileExtensions Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.<br><br>It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.<br><br>Defaults to `^[^hH][^tT][^mM][^lL]?$` which allows any file extension except HTML files.
207207
*/
208208

209209
/**

src/Options/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -493,9 +493,9 @@ export interface PasswordPolicyOptions {
493493
}
494494

495495
export interface FileUploadOptions {
496-
/* Allowed content types of files
497-
:DEFAULT: [] */
498-
fileExtensions: ?(string[]);
496+
/* Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.<br><br>It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.<br><br>Defaults to `^[^hH][^tT][^mM][^lL]?$` which allows any file extension except HTML files.
497+
:DEFAULT: ^[^hH][^tT][^mM][^lL]?$ */
498+
fileExtensions: ?string;
499499
/* Is true if file upload should be allowed for anonymous users.
500500
:DEFAULT: false */
501501
enableForAnonymousUser: ?boolean;

src/Routers/FilesRouter.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -138,17 +138,32 @@ export class FilesRouter {
138138
return;
139139
}
140140

141-
const fileExtensions = config.fileUpload && config.fileUpload.fileExtensions;
142-
if (!isMaster && fileExtensions && !fileExtensions.includes('*')) {
143-
try {
144-
const extension = filename.includes('.')
145-
? filename.split('.')[1]
146-
: contentType.split('/')[1];
147-
if (!fileExtensions.includes(extension)) {
148-
throw `File upload of type ${extension} is disabled.`;
141+
const fileExtensions = config.fileUpload?.fileExtensions;
142+
if (!isMaster && fileExtensions) {
143+
const isValidExtension = extension => {
144+
if (fileExtensions === '*') {
145+
return true;
146+
}
147+
if (Array.isArray(fileExtensions)) {
148+
if (fileExtensions.includes(extension)) {
149+
return true;
150+
}
151+
} else {
152+
const regex = new RegExp(fileExtensions);
153+
if (regex.test(extension)) {
154+
return true;
155+
}
149156
}
150-
} catch (e) {
151-
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, e));
157+
};
158+
let extension = filename.includes('.') ? filename.split('.')[1] : contentType.split('/')[1];
159+
extension = extension.split(' ').join('');
160+
if (!isValidExtension(extension)) {
161+
next(
162+
new Parse.Error(
163+
Parse.Error.FILE_SAVE_ERROR,
164+
`File upload of extension ${extension} is disabled.`
165+
)
166+
);
152167
return;
153168
}
154169
}

0 commit comments

Comments
 (0)