Skip to content

Commit 1c8d4a6

Browse files
Mike Patnodedplewis
Mike Patnode
authored andcommitted
Move filename validation out of the Router and into the FilesAdaptor (#6157)
* Move filename validation out of the Router and into the FilesAdaptor * Address PR comments * Update unittests to handle FilesAdapter interface change * Make validateFilename optional
1 parent 93fe6b4 commit 1c8d4a6

File tree

7 files changed

+93
-22
lines changed

7 files changed

+93
-22
lines changed

spec/AdaptableController.spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ describe('AdaptableController', () => {
6464
deleteFile: function() {},
6565
getFileData: function() {},
6666
getFileLocation: function() {},
67+
validateFilename: function() {},
6768
};
6869
expect(() => {
6970
new FilesController(adapter);
@@ -77,6 +78,7 @@ describe('AdaptableController', () => {
7778
AGoodAdapter.prototype.deleteFile = function() {};
7879
AGoodAdapter.prototype.getFileData = function() {};
7980
AGoodAdapter.prototype.getFileLocation = function() {};
81+
AGoodAdapter.prototype.validateFilename = function() {};
8082

8183
const adapter = new AGoodAdapter();
8284
expect(() => {

spec/FilesController.spec.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapte
44
.WinstonLoggerAdapter;
55
const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
66
.GridFSBucketAdapter;
7+
const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter')
8+
.GridStoreAdapter;
79
const Config = require('../lib/Config');
810
const FilesController = require('../lib/Controllers/FilesController').default;
911

@@ -14,6 +16,7 @@ const mockAdapter = {
1416
deleteFile: () => {},
1517
getFileData: () => {},
1618
getFileLocation: () => 'xyz',
19+
validateFilename: () => {},
1720
};
1821

1922
// Small additional tests to improve overall coverage
@@ -118,4 +121,22 @@ describe('FilesController', () => {
118121

119122
done();
120123
});
124+
125+
it('should reject slashes in file names', done => {
126+
const gridStoreAdapter = new GridFSBucketAdapter(
127+
'mongodb://localhost:27017/parse'
128+
);
129+
const fileName = 'foo/randomFileName.pdf';
130+
expect(gridStoreAdapter.validateFilename(fileName)).not.toBe(null);
131+
done();
132+
});
133+
134+
it('should also reject slashes in file names', done => {
135+
const gridStoreAdapter = new GridStoreAdapter(
136+
'mongodb://localhost:27017/parse'
137+
);
138+
const fileName = 'foo/randomFileName.pdf';
139+
expect(gridStoreAdapter.validateFilename(fileName)).not.toBe(null);
140+
done();
141+
});
121142
});

src/Adapters/Files/FilesAdapter.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@
88
// * deleteFile(filename)
99
// * getFileData(filename)
1010
// * getFileLocation(config, filename)
11+
// Adapter classes should implement the following functions:
12+
// * validateFilename(filename)
13+
// * handleFileStream(filename, req, res, contentType)
1114
//
1215
// Default is GridFSBucketAdapter, which requires mongo
1316
// and for the API server to be using the DatabaseController with Mongo
1417
// database adapter.
1518

1619
import type { Config } from '../../Config';
20+
import Parse from 'parse/node';
1721
/**
1822
* @module Adapters
1923
*/
@@ -56,6 +60,46 @@ export class FilesAdapter {
5660
* @return {string} Absolute URL
5761
*/
5862
getFileLocation(config: Config, filename: string): string {}
63+
64+
/** Validate a filename for this adapter type
65+
*
66+
* @param {string} filename
67+
*
68+
* @returns {null|Parse.Error} null if there are no errors
69+
*/
70+
// validateFilename(filename: string): ?Parse.Error {}
71+
72+
/** Handles Byte-Range Requests for Streaming
73+
*
74+
* @param {string} filename
75+
* @param {object} req
76+
* @param {object} res
77+
* @param {string} contentType
78+
*
79+
* @returns {Promise} Data for byte range
80+
*/
81+
// handleFileStream(filename: string, res: any, req: any, contentType: string): Promise
82+
}
83+
84+
/**
85+
* Simple filename validation
86+
*
87+
* @param filename
88+
* @returns {null|Parse.Error}
89+
*/
90+
export function validateFilename(filename): ?Parse.Error {
91+
if (filename.length > 128) {
92+
return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename too long.');
93+
}
94+
95+
const regx = /^[_a-zA-Z0-9][a-zA-Z0-9@. ~_-]*$/;
96+
if (!filename.match(regx)) {
97+
return new Parse.Error(
98+
Parse.Error.INVALID_FILE_NAME,
99+
'Filename contains invalid characters.'
100+
);
101+
}
102+
return null;
59103
}
60104

61105
export default FilesAdapter;

src/Adapters/Files/GridFSBucketAdapter.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
// @flow-disable-next
1010
import { MongoClient, GridFSBucket, Db } from 'mongodb';
11-
import { FilesAdapter } from './FilesAdapter';
11+
import { FilesAdapter, validateFilename } from './FilesAdapter';
1212
import defaults from '../../defaults';
1313

1414
export class GridFSBucketAdapter extends FilesAdapter {
@@ -139,6 +139,10 @@ export class GridFSBucketAdapter extends FilesAdapter {
139139
}
140140
return this._client.close(false);
141141
}
142+
143+
validateFilename(filename) {
144+
return validateFilename(filename);
145+
}
142146
}
143147

144148
export default GridFSBucketAdapter;

src/Adapters/Files/GridStoreAdapter.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
// @flow-disable-next
1111
import { MongoClient, GridStore, Db } from 'mongodb';
12-
import { FilesAdapter } from './FilesAdapter';
12+
import { FilesAdapter, validateFilename } from './FilesAdapter';
1313
import defaults from '../../defaults';
1414

1515
export class GridStoreAdapter extends FilesAdapter {
@@ -110,6 +110,10 @@ export class GridStoreAdapter extends FilesAdapter {
110110
}
111111
return this._client.close(false);
112112
}
113+
114+
validateFilename(filename) {
115+
return validateFilename(filename);
116+
}
113117
}
114118

115119
// handleRangeRequest is licensed under Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/).

src/Controllers/FilesController.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// FilesController.js
22
import { randomHexString } from '../cryptoUtils';
33
import AdaptableController from './AdaptableController';
4-
import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
4+
import { validateFilename, FilesAdapter } from '../Adapters/Files/FilesAdapter';
55
import path from 'path';
66
import mime from 'mime';
77

@@ -95,6 +95,13 @@ export class FilesController extends AdaptableController {
9595
handleFileStream(config, filename, req, res, contentType) {
9696
return this.adapter.handleFileStream(filename, req, res, contentType);
9797
}
98+
99+
validateFilename(filename) {
100+
if (typeof this.adapter.validateFilename === 'function') {
101+
return this.adapter.validateFilename(filename);
102+
}
103+
return validateFilename(filename);
104+
}
98105
}
99106

100107
export default FilesController;

src/Routers/FilesRouter.js

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -69,35 +69,24 @@ export class FilesRouter {
6969
}
7070

7171
createHandler(req, res, next) {
72+
const config = req.config;
73+
const filesController = config.filesController;
74+
const filename = req.params.filename;
75+
const contentType = req.get('Content-type');
76+
7277
if (!req.body || !req.body.length) {
7378
next(
7479
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.')
7580
);
7681
return;
7782
}
7883

79-
if (req.params.filename.length > 128) {
80-
next(
81-
new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename too long.')
82-
);
84+
const error = filesController.validateFilename(filename);
85+
if (error) {
86+
next(error);
8387
return;
8488
}
8589

86-
if (!req.params.filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
87-
next(
88-
new Parse.Error(
89-
Parse.Error.INVALID_FILE_NAME,
90-
'Filename contains invalid characters.'
91-
)
92-
);
93-
return;
94-
}
95-
96-
const filename = req.params.filename;
97-
const contentType = req.get('Content-type');
98-
const config = req.config;
99-
const filesController = config.filesController;
100-
10190
filesController
10291
.createFile(config, filename, req.body, contentType)
10392
.then(result => {

0 commit comments

Comments
 (0)