Skip to content

Feature - Allow extension preservation with safe file names #27

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

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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ Pass in non-Busboy options directly to the middleware. These are express-fileupl
Option | Acceptable Values | Details
--- | --- | ---
safeFileNames | <ul><li><code>false</code>&nbsp;**(default)**</li><li><code>true</code></li><li>regex</li></ul> | Strips characters from the upload's filename. You can use custom regex to determine what to strip. If set to `true`, non-alphanumeric characters _except_ dashes and underscores will be stripped. This option is off by default.<br /><br />**Example #1 (strip slashes from file names):** `app.use(fileUpload({ safeFileNames: /\\/g }))`<br />**Example #2:** `app.use(fileUpload({ safeFileNames: true }))`
preserveExtension | <ul><li><code>false</code>&nbsp;**(default)**</li><li><code>true</code></li><li><code>*Number*</code></li></ul> | Preserves filename extension when using <code>safeFileNames</code> option. If set to <code>true</code>, will default to an extension length of 3. If set to <code>*Number*</code>, this will be the max allowable extension length. If an extension is smaller than the extension length, it remains untouched. If the extension is longer, it is shifted.<br /><br />**Example #1 (true):**<br /><code>app.use(fileUpload({ safeFileNames: true, preserveExtension: true }));</code><br />*myFileName.ext* --> *myFileName.ext*<br /><br />**Example #2 (max extension length 2, extension shifted):**<br /><code>app.use(fileUpload({ safeFileNames: true, preserveExtension: 2 }));</code><br />*myFileName.ext* --> *myFileNamee.xt*

# Help Wanted
Pull Requests are welcomed!
Expand Down
36 changes: 31 additions & 5 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module.exports = function(options) {

return function(req, res, next) {
if (!hasBody(req) || !hasAcceptableMethod(req) || !hasAcceptableMime(req))
return next();
return next();

processMultipart(options, req, res, next);
};
Expand Down Expand Up @@ -85,14 +85,40 @@ function processMultipart(options, req, res, next) {
// see: https://github.com/richardgirges/express-fileupload/issues/14
// firefox uploads empty file in case of cache miss when f5ing page.
// resulting in unexpected behavior. if there is no file data, the file is invalid.
if(!buf.length)
if (!buf.length)
return;

if (options.safeFileNames) {
let maxExtensionLength = 3;
let extension = '';

if (typeof options.safeFileNames === 'object')
safeFileNameRegex = options.safeFileNames;

filename = filename.replace(safeFileNameRegex, '');
maxExtensionLength = parseInt(options.preserveExtension);
if (options.preserveExtension || maxExtensionLength === 0) {
if (isNaN(maxExtensionLength))
maxExtensionLength = 3;
else
maxExtensionLength = Math.abs(maxExtensionLength);

let filenameParts = filename.split('.');
let filenamePartsLen = filenameParts.length;
if (filenamePartsLen > 1) {
extension = filenameParts.pop();

if (extension.length > maxExtensionLength && maxExtensionLength > 0) {
filenameParts[filenameParts.length - 1] +=
'.' + extension.substr(0, extension.length - maxExtensionLength);
extension = extension.substr(-maxExtensionLength);
}

extension = maxExtensionLength ? '.' + extension.replace(safeFileNameRegex, '') : '';
filename = filenameParts.join('.');
}
}

filename = filename.replace(safeFileNameRegex, '').concat(extension);
}

let newFile = {
Expand Down Expand Up @@ -123,9 +149,9 @@ function processMultipart(options, req, res, next) {
} else {
// Array fields
if (req.files[fieldname] instanceof Array)
req.files[fieldname].push(newFile);
req.files[fieldname].push(newFile);
else
req.files[fieldname] = [req.files[fieldname], newFile];
req.files[fieldname] = [req.files[fieldname], newFile];
}
});
});
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "express-fileupload",
"version": "0.1.2",
"version": "0.1.3",
"author": "Richard Girges <[email protected]>",
"description": "Simple express file upload middleware that wraps around Busboy",
"main": "./lib/index",
Expand Down
Binary file added test/files/basket.ball.bp
Binary file not shown.
Binary file added test/files/my$Invalid#fileName.png123
Binary file not shown.
2 changes: 1 addition & 1 deletion test/multipartFields.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const request = require('supertest');
const server = require('./server');
const app = server.app;
const app = server.setup();

let mockUser = {
firstName: 'Joe',
Expand Down
2 changes: 1 addition & 1 deletion test/multipartUploads.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const fs = require('fs');
const path = require('path');
const request = require('supertest');
const server = require('./server');
const app = server.app;
const app = server.setup();
const clearUploadsDir = server.clearUploadsDir;
const fileDir = server.fileDir;
const uploadDir = server.uploadDir;
Expand Down
187 changes: 187 additions & 0 deletions test/options.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
const fs = require('fs');
const path = require('path');
const request = require('supertest');
const server = require('./server');
const clearUploadsDir = server.clearUploadsDir;
const fileDir = server.fileDir;
const uploadDir = server.uploadDir;

describe('File Upload Options Tests', function() {
afterEach(function(done) {
clearUploadsDir();
done();
});

/**
* Upload the file for testing and verify the expected filename.
* @param {object} options The expressFileUpload options.
* @param {string} actualFileNameToUpload The name of the file to upload.
* @param {string} expectedFileNameOnFileSystem The name of the file after upload.
* @param {function} done The mocha continuation function.
*/
function executeFileUploadTestWalk(options,
actualFileNameToUpload,
expectedFileNameOnFileSystem,
done) {
request(server.setup(options))
.post('/upload/single')
.attach('testFile', path.join(fileDir, actualFileNameToUpload))
.expect(200)
.end(function(err) {
if (err)
return done(err);

const uploadedFilePath = path.join(uploadDir, expectedFileNameOnFileSystem);

fs.stat(uploadedFilePath, done);
});
}

describe('Testing [safeFileNames] option to ensure:', function() {
it('Does nothing to your filename when disabled.',
function(done) {
const fileUploadOptions = {safeFileNames: false};
const actualFileName = 'my$Invalid#fileName.png123';
const expectedFileName = 'my$Invalid#fileName.png123';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});

it('Is disabled by default.',
function(done) {
const fileUploadOptions = null;
const actualFileName = 'my$Invalid#fileName.png123';
const expectedFileName = 'my$Invalid#fileName.png123';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});

it('Strips away all non-alphanumeric characters (excluding hyphens/underscores) when enabled.',
function(done) {
const fileUploadOptions = {safeFileNames: true};
const actualFileName = 'my$Invalid#fileName.png123';
const expectedFileName = 'myInvalidfileNamepng123';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});

it('Accepts a regex for stripping (decidedly) "invalid" characters from filename.',
function(done) {
const fileUploadOptions = {safeFileNames: /[\$#]/g};
const actualFileName = 'my$Invalid#fileName.png123';
const expectedFileName = 'myInvalidfileName.png123';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});
});

describe('Testing [preserveExtension] option to ensure:', function() {
it('Does not preserve the extension of your filename when disabled.',
function(done) {
const fileUploadOptions = {safeFileNames: true, preserveExtension: false};
const actualFileName = 'my$Invalid#fileName.png123';
const expectedFileName = 'myInvalidfileNamepng123';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});

it('Is disabled by default.',
function(done) {
const fileUploadOptions = {safeFileNames: true};
const actualFileName = 'my$Invalid#fileName.png123';
const expectedFileName = 'myInvalidfileNamepng123';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});

it('Shortens your extension to the default(3) when enabled, if the extension found is larger.',
function(done) {
const fileUploadOptions = {safeFileNames: true, preserveExtension: true};
const actualFileName = 'my$Invalid#fileName.png123';
const expectedFileName = 'myInvalidfileNamepng.123';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});

it('Leaves your extension alone when enabled, if the extension found is <= default(3) length',
function(done) {
const fileUploadOptions = {safeFileNames: true, preserveExtension: true};
const actualFileName = 'car.png';
const expectedFileName = 'car.png';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});

it('Can be configured for an extension length > default(3).',
function(done) {
const fileUploadOptions = {safeFileNames: true, preserveExtension: 7};
const actualFileName = 'my$Invalid#fileName.png123';
const expectedFileName = 'myInvalidfileName.png123';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});

it('Can be configured for an extension length < default(3).',
function(done) {
const fileUploadOptions = {safeFileNames: true, preserveExtension: 2};
const actualFileName = 'my$Invalid#fileName.png123';
const expectedFileName = 'myInvalidfileNamepng1.23';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});

it('Will use the absolute value of your extension length when negative.',
function(done) {
const fileUploadOptions = {safeFileNames: true, preserveExtension: -5};
const actualFileName = 'my$Invalid#fileName.png123';
const expectedFileName = 'myInvalidfileNamep.ng123';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});

it('Will leave no extension when the extension length == 0.',
function(done) {
const fileUploadOptions = {safeFileNames: true, preserveExtension: 0};
const actualFileName = 'car.png';
const expectedFileName = 'car';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});

it('Will accept numbers as strings, if they can be resolved with parseInt.',
function(done) {
const fileUploadOptions = {safeFileNames: true, preserveExtension: '3'};
const actualFileName = 'my$Invalid#fileName.png123';
const expectedFileName = 'myInvalidfileNamepng.123';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});

it('Will be evaluated for truthy-ness if it cannot be parsed as an int.',
function(done) {
const fileUploadOptions = {safeFileNames: true, preserveExtension: 'not-a-#-but-truthy'};
const actualFileName = 'my$Invalid#fileName.png123';
const expectedFileName = 'myInvalidfileNamepng.123';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});

it('Will ignore any decimal amount when evaluating for extension length.',
function(done) {
const fileUploadOptions = {safeFileNames: true, preserveExtension: 4.98};
const actualFileName = 'my$Invalid#fileName.png123';
const expectedFileName = 'myInvalidfileNamepn.g123';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});

it('Only considers the last dotted part as the extension.',
function(done) {
const fileUploadOptions = {safeFileNames: true, preserveExtension: true};
const actualFileName = 'basket.ball.bp';
const expectedFileName = 'basketball.bp';

executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
});
});
});
Loading