Skip to content
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

Move filename validation out of the Router and into the FilesAdaptor #6157

Merged
merged 4 commits into from
Oct 27, 2019
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
2 changes: 2 additions & 0 deletions spec/AdaptableController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe('AdaptableController', () => {
deleteFile: function() {},
getFileData: function() {},
getFileLocation: function() {},
validateFilename: function() {},
};
expect(() => {
new FilesController(adapter);
Expand All @@ -77,6 +78,7 @@ describe('AdaptableController', () => {
AGoodAdapter.prototype.deleteFile = function() {};
AGoodAdapter.prototype.getFileData = function() {};
AGoodAdapter.prototype.getFileLocation = function() {};
AGoodAdapter.prototype.validateFilename = function() {};

const adapter = new AGoodAdapter();
expect(() => {
Expand Down
21 changes: 21 additions & 0 deletions spec/FilesController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapte
.WinstonLoggerAdapter;
const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
.GridFSBucketAdapter;
const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter')
.GridStoreAdapter;
const Config = require('../lib/Config');
const FilesController = require('../lib/Controllers/FilesController').default;

Expand All @@ -14,6 +16,7 @@ const mockAdapter = {
deleteFile: () => {},
getFileData: () => {},
getFileLocation: () => 'xyz',
validateFilename: () => {},
};

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

done();
});

it('should reject slashes in file names', done => {
const gridStoreAdapter = new GridFSBucketAdapter(
'mongodb://localhost:27017/parse'
);
const fileName = 'foo/randomFileName.pdf';
expect(gridStoreAdapter.validateFilename(fileName)).not.toBe(null);
done();
});

it('should also reject slashes in file names', done => {
const gridStoreAdapter = new GridStoreAdapter(
'mongodb://localhost:27017/parse'
);
const fileName = 'foo/randomFileName.pdf';
expect(gridStoreAdapter.validateFilename(fileName)).not.toBe(null);
done();
});
});
44 changes: 44 additions & 0 deletions src/Adapters/Files/FilesAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
// * deleteFile(filename)
// * getFileData(filename)
// * getFileLocation(config, filename)
// Adapter classes should implement the following functions:
// * validateFilename(filename)
// * handleFileStream(filename, req, res, contentType)
//
// Default is GridFSBucketAdapter, which requires mongo
// and for the API server to be using the DatabaseController with Mongo
// database adapter.

import type { Config } from '../../Config';
import Parse from 'parse/node';
/**
* @module Adapters
*/
Expand Down Expand Up @@ -56,6 +60,46 @@ export class FilesAdapter {
* @return {string} Absolute URL
*/
getFileLocation(config: Config, filename: string): string {}

/** Validate a filename for this adapter type
*
* @param {string} filename
*
* @returns {null|Parse.Error} null if there are no errors
*/
// validateFilename(filename: string): ?Parse.Error {}

/** Handles Byte-Range Requests for Streaming
*
* @param {string} filename
* @param {object} req
* @param {object} res
* @param {string} contentType
*
* @returns {Promise} Data for byte range
*/
// handleFileStream(filename: string, res: any, req: any, contentType: string): Promise
}

/**
* Simple filename validation
*
* @param filename
* @returns {null|Parse.Error}
*/
export function validateFilename(filename): ?Parse.Error {
dplewis marked this conversation as resolved.
Show resolved Hide resolved
if (filename.length > 128) {
return new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename too long.');
}

const regx = /^[_a-zA-Z0-9][a-zA-Z0-9@. ~_-]*$/;
dplewis marked this conversation as resolved.
Show resolved Hide resolved
if (!filename.match(regx)) {
return new Parse.Error(
Parse.Error.INVALID_FILE_NAME,
'Filename contains invalid characters.'
);
}
return null;
}

export default FilesAdapter;
6 changes: 5 additions & 1 deletion src/Adapters/Files/GridFSBucketAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

// @flow-disable-next
import { MongoClient, GridFSBucket, Db } from 'mongodb';
import { FilesAdapter } from './FilesAdapter';
import { FilesAdapter, validateFilename } from './FilesAdapter';
import defaults from '../../defaults';

export class GridFSBucketAdapter extends FilesAdapter {
Expand Down Expand Up @@ -139,6 +139,10 @@ export class GridFSBucketAdapter extends FilesAdapter {
}
return this._client.close(false);
}

validateFilename(filename) {
return validateFilename(filename);
}
dplewis marked this conversation as resolved.
Show resolved Hide resolved
}

export default GridFSBucketAdapter;
6 changes: 5 additions & 1 deletion src/Adapters/Files/GridStoreAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

// @flow-disable-next
import { MongoClient, GridStore, Db } from 'mongodb';
import { FilesAdapter } from './FilesAdapter';
import { FilesAdapter, validateFilename } from './FilesAdapter';
import defaults from '../../defaults';

export class GridStoreAdapter extends FilesAdapter {
Expand Down Expand Up @@ -110,6 +110,10 @@ export class GridStoreAdapter extends FilesAdapter {
}
return this._client.close(false);
}

validateFilename(filename) {
return validateFilename(filename);
dplewis marked this conversation as resolved.
Show resolved Hide resolved
}
}

// handleRangeRequest is licensed under Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/).
Expand Down
9 changes: 8 additions & 1 deletion src/Controllers/FilesController.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// FilesController.js
import { randomHexString } from '../cryptoUtils';
import AdaptableController from './AdaptableController';
import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
import { validateFilename, FilesAdapter } from '../Adapters/Files/FilesAdapter';
import path from 'path';
import mime from 'mime';

Expand Down Expand Up @@ -95,6 +95,13 @@ export class FilesController extends AdaptableController {
handleFileStream(config, filename, req, res, contentType) {
return this.adapter.handleFileStream(filename, req, res, contentType);
}

validateFilename(filename) {
if (typeof this.adapter.validateFilename === 'function') {
return this.adapter.validateFilename(filename);
}
return validateFilename(filename);
}
}

export default FilesController;
27 changes: 8 additions & 19 deletions src/Routers/FilesRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,35 +69,24 @@ export class FilesRouter {
}

createHandler(req, res, next) {
const config = req.config;
const filesController = config.filesController;
const filename = req.params.filename;
const contentType = req.get('Content-type');

if (!req.body || !req.body.length) {
next(
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.')
);
return;
}

if (req.params.filename.length > 128) {
next(
new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename too long.')
);
const error = filesController.validateFilename(filename);
if (error) {
next(error);
return;
}

if (!req.params.filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
next(
new Parse.Error(
Parse.Error.INVALID_FILE_NAME,
'Filename contains invalid characters.'
)
);
return;
}

const filename = req.params.filename;
const contentType = req.get('Content-type');
const config = req.config;
const filesController = config.filesController;

filesController
.createFile(config, filename, req.body, contentType)
.then(result => {
Expand Down