You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
468 lines
14 KiB
468 lines
14 KiB
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const md5 = require('md5');
|
|
const server = require('./server');
|
|
const fileDir = server.fileDir;
|
|
const uploadDir = server.uploadDir;
|
|
const {
|
|
debugLog,
|
|
isFunc,
|
|
errorFunc,
|
|
getTempFilename,
|
|
buildOptions,
|
|
buildFields,
|
|
checkAndMakeDir,
|
|
deleteFile,
|
|
copyFile,
|
|
saveBufferToFile,
|
|
parseFileName,
|
|
uriDecodeFileName,
|
|
isSafeFromPollution
|
|
} = require('../lib/utilities');
|
|
|
|
const mockFile = 'basketball.png';
|
|
const mockBuffer = fs.readFileSync(path.join(fileDir, mockFile));
|
|
const mockHash = md5(mockBuffer);
|
|
|
|
|
|
describe('Test of the utilities functions', function() {
|
|
beforeEach(function() {
|
|
server.clearUploadsDir();
|
|
});
|
|
//debugLog tests
|
|
describe('Test debugLog function', () => {
|
|
|
|
let testMessage = 'Test message';
|
|
|
|
it('debugLog returns false if no options passed', () => {
|
|
assert.equal(debugLog(null, testMessage), false);
|
|
});
|
|
|
|
it('debugLog returns false if option debug is false', () => {
|
|
assert.equal(debugLog({debug: false}, testMessage), false);
|
|
});
|
|
|
|
it('debugLog returns true if option debug is true', () => {
|
|
assert.equal(debugLog({debug: true}, testMessage), true);
|
|
});
|
|
|
|
});
|
|
//isFunc tests
|
|
describe('Test isFunc function', () => {
|
|
|
|
it('isFunc returns true if function passed', () => assert.equal(isFunc(()=>{}), true));
|
|
|
|
it('isFunc returns false if null passed', function() {
|
|
assert.equal(isFunc(null), false);
|
|
});
|
|
|
|
it('isFunc returns false if undefined passed', function() {
|
|
assert.equal(isFunc(undefined), false);
|
|
});
|
|
|
|
it('isFunc returns false if object passed', function() {
|
|
assert.equal(isFunc({}), false);
|
|
});
|
|
|
|
it('isFunc returns false if array passed', function() {
|
|
assert.equal(isFunc([]), false);
|
|
});
|
|
});
|
|
//errorFunc tests
|
|
describe('Test errorFunc function', () => {
|
|
|
|
const resolve = () => 'success';
|
|
const reject = () => 'error';
|
|
|
|
it('errorFunc returns resolve if reject function has not been passed', () => {
|
|
let result = errorFunc(resolve);
|
|
assert.equal(result(), 'success');
|
|
});
|
|
|
|
it('errorFunc returns reject if reject function has been passed', () => {
|
|
let result = errorFunc(resolve, reject);
|
|
assert.equal(result(), 'error');
|
|
});
|
|
|
|
});
|
|
//getTempFilename tests
|
|
describe('Test getTempFilename function', () => {
|
|
|
|
const nameRegexp = /tmp-\d{1,5}-\d{1,}/;
|
|
|
|
it('getTempFilename result matches regexp /tmp-d{1,5}-d{1,}/', () => {
|
|
|
|
let errCounter = 0;
|
|
let tempName = '';
|
|
for (var i = 0; i < 65537; i++) {
|
|
tempName = getTempFilename();
|
|
if (!nameRegexp.test(tempName)) errCounter ++;
|
|
}
|
|
|
|
assert.equal(errCounter, 0);
|
|
});
|
|
|
|
it('getTempFilename current and previous results are not equal', () => {
|
|
|
|
let errCounter = 0;
|
|
let tempName = '';
|
|
let previousName = '';
|
|
for (var i = 0; i < 65537; i++) {
|
|
previousName = tempName;
|
|
tempName = getTempFilename();
|
|
if (previousName === tempName) errCounter ++;
|
|
}
|
|
|
|
assert.equal(errCounter, 0);
|
|
});
|
|
|
|
});
|
|
//parseFileName
|
|
describe('Test parseFileName function', () => {
|
|
|
|
it('Does nothing to your filename when disabled.', () => {
|
|
const opts = {safeFileNames: false};
|
|
const name = 'my$Invalid#fileName.png123';
|
|
const expected = 'my$Invalid#fileName.png123';
|
|
let result = parseFileName(opts, name);
|
|
assert.equal(result, expected);
|
|
});
|
|
|
|
it('Cuts of file name length if it more then 255 chars.', () => {
|
|
const name = 'a'.repeat(300);
|
|
const result = parseFileName({}, name);
|
|
assert.equal(result.length, 255);
|
|
});
|
|
|
|
it(
|
|
'Strips away all non-alphanumeric characters (excluding hyphens/underscores) when enabled.',
|
|
() => {
|
|
const opts = {safeFileNames: true};
|
|
const name = 'my$Invalid#fileName.png123';
|
|
const expected = 'myInvalidfileNamepng123';
|
|
let result = parseFileName(opts, name);
|
|
assert.equal(result, expected);
|
|
});
|
|
|
|
it(
|
|
'Strips away all non-alphanumeric chars when preserveExtension: true for a name without dots',
|
|
() => {
|
|
const opts = {safeFileNames: true, preserveExtension: true};
|
|
const name = 'my$Invalid#fileName';
|
|
const expected = 'myInvalidfileName';
|
|
let result = parseFileName(opts, name);
|
|
assert.equal(result, expected);
|
|
});
|
|
|
|
it('Accepts a regex for stripping (decidedly) "invalid" characters from filename.', () => {
|
|
const opts = {safeFileNames: /[$#]/g};
|
|
const name = 'my$Invalid#fileName.png123';
|
|
const expected = 'myInvalidfileName.png123';
|
|
let result = parseFileName(opts, name);
|
|
assert.equal(result, expected);
|
|
});
|
|
|
|
it(
|
|
'Returns correct filename if name contains dots characters and preserveExtension: true.',
|
|
() => {
|
|
const opts = {safeFileNames: true, preserveExtension: true};
|
|
const name = 'basket.ball.png';
|
|
const expected = 'basketball.png';
|
|
let result = parseFileName(opts, name);
|
|
assert.equal(result, expected);
|
|
});
|
|
|
|
it('Returns a temporary file name if name argument is empty.', () => {
|
|
const opts = {safeFileNames: false};
|
|
const result = parseFileName(opts);
|
|
assert.equal(typeof result, 'string');
|
|
});
|
|
|
|
});
|
|
//buildOptions tests
|
|
describe('Test buildOptions function', () => {
|
|
|
|
const source = { option1: '1', option2: '2' };
|
|
const sourceAddon = { option3: '3'};
|
|
const expected = { option1: '1', option2: '2' };
|
|
const expectedAddon = { option1: '1', option2: '2', option3: '3'};
|
|
|
|
it('buildOptions returns and equal object to the object which was paased', () => {
|
|
let result = buildOptions(source);
|
|
assert.deepStrictEqual(result, source);
|
|
});
|
|
|
|
it('buildOptions doesnt add non object or null arguments to the result', () => {
|
|
let result = buildOptions(source, 2, '3', null);
|
|
assert.deepStrictEqual(result, expected);
|
|
});
|
|
|
|
it('buildOptions adds value to the result from the several source argumets', () => {
|
|
let result = buildOptions(source, sourceAddon);
|
|
assert.deepStrictEqual(result, expectedAddon);
|
|
});
|
|
|
|
});
|
|
//buildFields tests
|
|
describe('Test buildOptions function', () => {
|
|
|
|
it('buildFields does nothing if null value has been passed', () => {
|
|
let fields = null;
|
|
fields = buildFields(fields, 'test', null);
|
|
assert.equal(fields, null);
|
|
});
|
|
|
|
});
|
|
//checkAndMakeDir tests
|
|
describe('Test checkAndMakeDir function', () => {
|
|
//
|
|
it('checkAndMakeDir returns false if upload options object was not set', () => {
|
|
assert.equal(checkAndMakeDir(), false);
|
|
});
|
|
//
|
|
it('checkAndMakeDir returns false if upload option createParentPath was not set', () => {
|
|
assert.equal(checkAndMakeDir({}), false);
|
|
});
|
|
//
|
|
it('checkAndMakeDir returns false if filePath was not set', () => {
|
|
assert.equal(checkAndMakeDir({createParentPath: true}), false);
|
|
});
|
|
//
|
|
it('checkAndMakeDir return true if path to the file already exists', ()=>{
|
|
let dir = path.join(uploadDir, 'testfile');
|
|
assert.equal(checkAndMakeDir({createParentPath: true}, dir), true);
|
|
});
|
|
//
|
|
it('checkAndMakeDir creates a dir if path to the file not exists', ()=>{
|
|
let dir = path.join(uploadDir, 'testfolder', 'testfile');
|
|
assert.equal(checkAndMakeDir({createParentPath: true}, dir), true);
|
|
});
|
|
//
|
|
it('checkAndMakeDir creates a dir recursively if path to the file not exists', ()=>{
|
|
let dir = path.join(uploadDir, 'testfolder', 'testsubfolder', 'testfile');
|
|
assert.equal(checkAndMakeDir({createParentPath: true}, dir), true);
|
|
});
|
|
});
|
|
//saveBufferToFile tests
|
|
describe('Test saveBufferToFile function', function(){
|
|
beforeEach(function() {
|
|
server.clearUploadsDir();
|
|
});
|
|
|
|
it('Save buffer to a file', function(done) {
|
|
let filePath = path.join(uploadDir, mockFile);
|
|
saveBufferToFile(mockBuffer, filePath, function(err){
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
fs.stat(filePath, done);
|
|
});
|
|
});
|
|
|
|
it('Failed if not a buffer passed', function(done) {
|
|
let filePath = path.join(uploadDir, mockFile);
|
|
saveBufferToFile(undefined, filePath, function(err){
|
|
if (err) {
|
|
return done();
|
|
}
|
|
});
|
|
});
|
|
|
|
it('Failed if wrong path passed', function(done) {
|
|
let filePath = '';
|
|
saveBufferToFile(mockFile, filePath, function(err){
|
|
if (err) {
|
|
return done();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Test deleteFile function', function(){
|
|
beforeEach(function() {
|
|
server.clearUploadsDir();
|
|
});
|
|
|
|
it('Failed if nonexistent file passed', function(done){
|
|
let filePath = path.join(uploadDir, getTempFilename());
|
|
|
|
deleteFile(filePath, function(err){
|
|
if (err) {
|
|
return done();
|
|
}
|
|
});
|
|
});
|
|
|
|
it('Delete a file', function(done){
|
|
let srcPath = path.join(fileDir, mockFile);
|
|
let dstPath = path.join(uploadDir, getTempFilename());
|
|
|
|
//copy a file
|
|
copyFile(srcPath, dstPath, function(err){
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
fs.stat(dstPath, (err)=>{
|
|
if (err){
|
|
return done(err);
|
|
}
|
|
// delete a file
|
|
deleteFile(dstPath, function(err){
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
fs.stat(dstPath, (err)=>{
|
|
if (err){
|
|
return done();
|
|
}
|
|
|
|
//error if a file still exist
|
|
done(err);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
describe('Test copyFile function', function(){
|
|
beforeEach(function() {
|
|
server.clearUploadsDir();
|
|
});
|
|
|
|
it('Copy a file and check a hash', function(done) {
|
|
let srcPath = path.join(fileDir, mockFile);
|
|
let dstPath = path.join(uploadDir, mockFile);
|
|
|
|
copyFile(srcPath, dstPath, function(err){
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
fs.stat(dstPath, (err)=>{
|
|
if (err){
|
|
return done(err);
|
|
}
|
|
//Match source and destination files hash.
|
|
let fileBuffer = fs.readFileSync(dstPath);
|
|
let fileHash = md5(fileBuffer);
|
|
return (fileHash === mockHash) ? done() : done(err);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('Failed if wrong source file path passed', function(done){
|
|
let srcPath = path.join(fileDir, 'unknown');
|
|
let dstPath = path.join(uploadDir, mockFile);
|
|
|
|
copyFile(srcPath, dstPath, function(err){
|
|
if (err) {
|
|
return done();
|
|
}
|
|
});
|
|
});
|
|
|
|
it('Failed if wrong destination file path passed', function(done){
|
|
let srcPath = path.join(fileDir, 'unknown');
|
|
let dstPath = path.join('unknown', 'unknown');
|
|
|
|
copyFile(srcPath, dstPath, function(err){
|
|
if (err) {
|
|
return done();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Test uriDecodeFileName function', function() {
|
|
const testData = [
|
|
{ enc: 'test%22filename', dec: 'test"filename' },
|
|
{ enc: 'test%60filename', dec: 'test`filename' },
|
|
{ enc: '%3Fx%3Dtest%22filename', dec: '?x=test"filename'}
|
|
];
|
|
|
|
// Test decoding if uriDecodeFileNames: true.
|
|
testData.forEach((testName) => {
|
|
const opts = { uriDecodeFileNames: true };
|
|
it(`Return ${testName.dec} for input ${testName.enc} if uriDecodeFileNames: true`, () => {
|
|
assert.equal(uriDecodeFileName(opts, testName.enc), testName.dec);
|
|
});
|
|
});
|
|
|
|
// Test decoding if uriDecodeFileNames: false.
|
|
testData.forEach((testName) => {
|
|
const opts = { uriDecodeFileNames: false };
|
|
it(`Return ${testName.enc} for input ${testName.enc} if uriDecodeFileNames: false`, () => {
|
|
assert.equal(uriDecodeFileName(opts, testName.enc), testName.enc);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Test for no prototype pollution in buildFields', function() {
|
|
const prototypeFields = [
|
|
{ name: '__proto__', data: {} },
|
|
{ name: 'constructor', data: {} },
|
|
{ name: 'toString', data: {} }
|
|
];
|
|
|
|
const nonPrototypeFields = [
|
|
{ name: 'a', data: {} },
|
|
{ name: 'b', data: {} }
|
|
];
|
|
|
|
let fieldObject = undefined;
|
|
[...prototypeFields, ...nonPrototypeFields].forEach((field) => {
|
|
fieldObject = buildFields(fieldObject, field.name, field.data);
|
|
});
|
|
|
|
it(`Has ${nonPrototypeFields.length} keys`, () => {
|
|
assert.equal(Object.keys(fieldObject).length, nonPrototypeFields.length);
|
|
});
|
|
|
|
it(`Has null as its prototype`, () => {
|
|
assert.equal(Object.getPrototypeOf(fieldObject), null);
|
|
});
|
|
|
|
prototypeFields.forEach((field) => {
|
|
it(`${field.name} property is not an array`, () => {
|
|
// Note, Array.isArray is an insufficient test due to it returning false
|
|
// for Objects with an array prototype.
|
|
assert.equal(fieldObject[field.name] instanceof Array, false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Test for correct detection of prototype pollution', function() {
|
|
const validInsertions = [
|
|
{ base: {}, key: 'a' },
|
|
{ base: { a: 1 }, key: 'a' },
|
|
{ base: { __proto__: { a: 1 } }, key: 'a' },
|
|
{ base: [1], key: 0 },
|
|
{ base: { __proto__: [1] }, key: 0 }
|
|
];
|
|
|
|
const invalidInsertions = [
|
|
{ base: {}, key: '__proto__' },
|
|
{ base: {}, key: 'constructor' },
|
|
{ base: [1], key: '__proto__' },
|
|
{ base: [1], key: 'length' },
|
|
{ base: { __proto__: [1] }, key: 'length' }
|
|
];
|
|
|
|
validInsertions.forEach((insertion) => {
|
|
it(`Key ${insertion.key} should be valid for ${JSON.stringify(insertion.base)}`, () => {
|
|
assert.equal(isSafeFromPollution(insertion.base, insertion.key), true);
|
|
});
|
|
});
|
|
|
|
invalidInsertions.forEach((insertion) => {
|
|
it(`Key ${insertion.key} should not be valid for ${JSON.stringify(insertion.base)}`, () => {
|
|
assert.equal(isSafeFromPollution(insertion.base, insertion.key), false);
|
|
});
|
|
});
|
|
});
|
|
});
|