Skip to content

Commit 7f52557

Browse files
committed
fix-release
1 parent f7eee19 commit 7f52557

File tree

6 files changed

+209
-25
lines changed

6 files changed

+209
-25
lines changed

spec/ParseFile.spec.js

+161-25
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 => {
@@ -1298,6 +1304,136 @@ describe('Parse.File testing', () => {
12981304
await expectAsync(reconfigureServer({ fileUpload: { [key]: value } })).toBeResolved();
12991305
}
13001306
}
1307+
await expectAsync(
1308+
reconfigureServer({
1309+
fileUpload: {
1310+
fileExtensions: 1,
1311+
},
1312+
})
1313+
).toBeRejectedWith('fileUpload.fileExtensions must be an array.');
1314+
});
1315+
});
1316+
1317+
describe('fileExtensions', () => {
1318+
it('works with _ContentType', async () => {
1319+
await reconfigureServer({
1320+
fileUpload: {
1321+
enableForPublic: true,
1322+
fileExtensions: ['png'],
1323+
},
1324+
});
1325+
await expectAsync(
1326+
request({
1327+
method: 'POST',
1328+
url: 'http://localhost:8378/1/files/file',
1329+
body: JSON.stringify({
1330+
_ApplicationId: 'test',
1331+
_JavaScriptKey: 'test',
1332+
_ContentType: 'text/html',
1333+
base64: 'PGh0bWw+PC9odG1sPgo=',
1334+
}),
1335+
}).catch(e => {
1336+
throw new Error(e.data.error);
1337+
})
1338+
).toBeRejectedWith(
1339+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
1340+
);
1341+
});
1342+
1343+
it('works without Content-Type', async () => {
1344+
await reconfigureServer({
1345+
fileUpload: {
1346+
enableForPublic: true,
1347+
},
1348+
});
1349+
const headers = {
1350+
'X-Parse-Application-Id': 'test',
1351+
'X-Parse-REST-API-Key': 'rest',
1352+
};
1353+
await expectAsync(
1354+
request({
1355+
method: 'POST',
1356+
headers: headers,
1357+
url: 'http://localhost:8378/1/files/file.html',
1358+
body: '<html></html>\n',
1359+
}).catch(e => {
1360+
throw new Error(e.data.error);
1361+
})
1362+
).toBeRejectedWith(
1363+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
1364+
);
1365+
});
1366+
1367+
it('works with array', async () => {
1368+
await reconfigureServer({
1369+
fileUpload: {
1370+
enableForPublic: true,
1371+
fileExtensions: ['jpg'],
1372+
},
1373+
});
1374+
await expectAsync(
1375+
request({
1376+
method: 'POST',
1377+
url: 'http://localhost:8378/1/files/file',
1378+
body: JSON.stringify({
1379+
_ApplicationId: 'test',
1380+
_JavaScriptKey: 'test',
1381+
_ContentType: 'text/html',
1382+
base64: 'PGh0bWw+PC9odG1sPgo=',
1383+
}),
1384+
}).catch(e => {
1385+
throw new Error(e.data.error);
1386+
})
1387+
).toBeRejectedWith(
1388+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
1389+
);
1390+
});
1391+
1392+
it('works with array without Content-Type', async () => {
1393+
await reconfigureServer({
1394+
fileUpload: {
1395+
enableForPublic: true,
1396+
fileExtensions: ['jpg'],
1397+
},
1398+
});
1399+
const headers = {
1400+
'X-Parse-Application-Id': 'test',
1401+
'X-Parse-REST-API-Key': 'rest',
1402+
};
1403+
await expectAsync(
1404+
request({
1405+
method: 'POST',
1406+
headers: headers,
1407+
url: 'http://localhost:8378/1/files/file.html',
1408+
body: '<html></html>\n',
1409+
}).catch(e => {
1410+
throw new Error(e.data.error);
1411+
})
1412+
).toBeRejectedWith(
1413+
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
1414+
);
1415+
});
1416+
1417+
it('works with array with correct file type', async () => {
1418+
await reconfigureServer({
1419+
fileUpload: {
1420+
enableForPublic: true,
1421+
fileExtensions: ['html'],
1422+
},
1423+
});
1424+
const response = await request({
1425+
method: 'POST',
1426+
url: 'http://localhost:8378/1/files/file',
1427+
body: JSON.stringify({
1428+
_ApplicationId: 'test',
1429+
_JavaScriptKey: 'test',
1430+
_ContentType: 'text/html',
1431+
base64: 'PGh0bWw+PC9odG1sPgo=',
1432+
}),
1433+
});
1434+
const b = response.data;
1435+
expect(b.name).toMatch(/_file.html$/);
1436+
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
13011437
});
13021438
});
13031439
});

src/Config.js

+5
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,11 @@ export class Config {
445445
} else if (typeof fileUpload.enableForAuthenticatedUser !== 'boolean') {
446446
throw 'fileUpload.enableForAuthenticatedUser must be a boolean value.';
447447
}
448+
if (fileUpload.fileExtensions === undefined) {
449+
fileUpload.fileExtensions = FileUploadOptions.fileExtensions.default;
450+
} else if (!Array.isArray(fileUpload.fileExtensions)) {
451+
throw 'fileUpload.fileExtensions must be an array.';
452+
}
448453
}
449454

450455
static validateIps(field, masterKeyIps) {

src/Options/Definitions.js

+7
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,13 @@ module.exports.FileUploadOptions = {
955955
action: parsers.booleanParser,
956956
default: false,
957957
},
958+
fileExtensions: {
959+
env: 'PARSE_SERVER_FILE_UPLOAD_FILE_EXTENSIONS',
960+
help:
961+
"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.",
962+
action: parsers.arrayParser,
963+
default: ['^[^hH][^tT][^mM][^lL]?$'],
964+
},
958965
};
959966
module.exports.DatabaseOptions = {
960967
enableSchemaHooks: {

src/Options/docs.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Options/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,9 @@ export interface PasswordPolicyOptions {
528528
}
529529

530530
export interface FileUploadOptions {
531+
/* 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.
532+
:DEFAULT: ["^[^hH][^tT][^mM][^lL]?$"] */
533+
fileExtensions: ?(string[]);
531534
/* Is true if file upload should be allowed for anonymous users.
532535
:DEFAULT: false */
533536
enableForAnonymousUser: ?boolean;

src/Routers/FilesRouter.js

+32
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,38 @@ export class FilesRouter {
140140
return;
141141
}
142142

143+
const fileExtensions = config.fileUpload?.fileExtensions;
144+
if (!isMaster && fileExtensions) {
145+
const isValidExtension = extension => {
146+
return fileExtensions.some(ext => {
147+
if (ext === '*') {
148+
return true;
149+
}
150+
const regex = new RegExp(fileExtensions);
151+
if (regex.test(extension)) {
152+
return true;
153+
}
154+
});
155+
};
156+
let extension = contentType;
157+
if (filename && filename.includes('.')) {
158+
extension = filename.split('.')[1];
159+
} else if (contentType && contentType.includes('/')) {
160+
extension = contentType.split('/')[1];
161+
}
162+
extension = extension.split(' ').join('');
163+
164+
if (!isValidExtension(extension)) {
165+
next(
166+
new Parse.Error(
167+
Parse.Error.FILE_SAVE_ERROR,
168+
`File upload of extension ${extension} is disabled.`
169+
)
170+
);
171+
return;
172+
}
173+
}
174+
143175
const base64 = req.body.toString('base64');
144176
const file = new Parse.File(filename, { base64 }, contentType);
145177
const { metadata = {}, tags = {} } = req.fileData || {};

0 commit comments

Comments
 (0)