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

  1. 'use strict';
  2. const assert = require('assert');
  3. const path = require('path');
  4. const fs = require('fs');
  5. const md5 = require('md5');
  6. const server = require('./server');
  7. const fileDir = server.fileDir;
  8. const uploadDir = server.uploadDir;
  9. const {
  10. debugLog,
  11. isFunc,
  12. errorFunc,
  13. getTempFilename,
  14. buildOptions,
  15. buildFields,
  16. checkAndMakeDir,
  17. deleteFile,
  18. copyFile,
  19. saveBufferToFile,
  20. parseFileName,
  21. uriDecodeFileName,
  22. isSafeFromPollution
  23. } = require('../lib/utilities');
  24. const mockFile = 'basketball.png';
  25. const mockBuffer = fs.readFileSync(path.join(fileDir, mockFile));
  26. const mockHash = md5(mockBuffer);
  27. describe('Test of the utilities functions', function() {
  28. beforeEach(function() {
  29. server.clearUploadsDir();
  30. });
  31. //debugLog tests
  32. describe('Test debugLog function', () => {
  33. let testMessage = 'Test message';
  34. it('debugLog returns false if no options passed', () => {
  35. assert.equal(debugLog(null, testMessage), false);
  36. });
  37. it('debugLog returns false if option debug is false', () => {
  38. assert.equal(debugLog({debug: false}, testMessage), false);
  39. });
  40. it('debugLog returns true if option debug is true', () => {
  41. assert.equal(debugLog({debug: true}, testMessage), true);
  42. });
  43. });
  44. //isFunc tests
  45. describe('Test isFunc function', () => {
  46. it('isFunc returns true if function passed', () => assert.equal(isFunc(()=>{}), true));
  47. it('isFunc returns false if null passed', function() {
  48. assert.equal(isFunc(null), false);
  49. });
  50. it('isFunc returns false if undefined passed', function() {
  51. assert.equal(isFunc(undefined), false);
  52. });
  53. it('isFunc returns false if object passed', function() {
  54. assert.equal(isFunc({}), false);
  55. });
  56. it('isFunc returns false if array passed', function() {
  57. assert.equal(isFunc([]), false);
  58. });
  59. });
  60. //errorFunc tests
  61. describe('Test errorFunc function', () => {
  62. const resolve = () => 'success';
  63. const reject = () => 'error';
  64. it('errorFunc returns resolve if reject function has not been passed', () => {
  65. let result = errorFunc(resolve);
  66. assert.equal(result(), 'success');
  67. });
  68. it('errorFunc returns reject if reject function has been passed', () => {
  69. let result = errorFunc(resolve, reject);
  70. assert.equal(result(), 'error');
  71. });
  72. });
  73. //getTempFilename tests
  74. describe('Test getTempFilename function', () => {
  75. const nameRegexp = /tmp-\d{1,5}-\d{1,}/;
  76. it('getTempFilename result matches regexp /tmp-d{1,5}-d{1,}/', () => {
  77. let errCounter = 0;
  78. let tempName = '';
  79. for (var i = 0; i < 65537; i++) {
  80. tempName = getTempFilename();
  81. if (!nameRegexp.test(tempName)) errCounter ++;
  82. }
  83. assert.equal(errCounter, 0);
  84. });
  85. it('getTempFilename current and previous results are not equal', () => {
  86. let errCounter = 0;
  87. let tempName = '';
  88. let previousName = '';
  89. for (var i = 0; i < 65537; i++) {
  90. previousName = tempName;
  91. tempName = getTempFilename();
  92. if (previousName === tempName) errCounter ++;
  93. }
  94. assert.equal(errCounter, 0);
  95. });
  96. });
  97. //parseFileName
  98. describe('Test parseFileName function', () => {
  99. it('Does nothing to your filename when disabled.', () => {
  100. const opts = {safeFileNames: false};
  101. const name = 'my$Invalid#fileName.png123';
  102. const expected = 'my$Invalid#fileName.png123';
  103. let result = parseFileName(opts, name);
  104. assert.equal(result, expected);
  105. });
  106. it('Cuts of file name length if it more then 255 chars.', () => {
  107. const name = 'a'.repeat(300);
  108. const result = parseFileName({}, name);
  109. assert.equal(result.length, 255);
  110. });
  111. it(
  112. 'Strips away all non-alphanumeric characters (excluding hyphens/underscores) when enabled.',
  113. () => {
  114. const opts = {safeFileNames: true};
  115. const name = 'my$Invalid#fileName.png123';
  116. const expected = 'myInvalidfileNamepng123';
  117. let result = parseFileName(opts, name);
  118. assert.equal(result, expected);
  119. });
  120. it(
  121. 'Strips away all non-alphanumeric chars when preserveExtension: true for a name without dots',
  122. () => {
  123. const opts = {safeFileNames: true, preserveExtension: true};
  124. const name = 'my$Invalid#fileName';
  125. const expected = 'myInvalidfileName';
  126. let result = parseFileName(opts, name);
  127. assert.equal(result, expected);
  128. });
  129. it('Accepts a regex for stripping (decidedly) "invalid" characters from filename.', () => {
  130. const opts = {safeFileNames: /[$#]/g};
  131. const name = 'my$Invalid#fileName.png123';
  132. const expected = 'myInvalidfileName.png123';
  133. let result = parseFileName(opts, name);
  134. assert.equal(result, expected);
  135. });
  136. it(
  137. 'Returns correct filename if name contains dots characters and preserveExtension: true.',
  138. () => {
  139. const opts = {safeFileNames: true, preserveExtension: true};
  140. const name = 'basket.ball.png';
  141. const expected = 'basketball.png';
  142. let result = parseFileName(opts, name);
  143. assert.equal(result, expected);
  144. });
  145. it('Returns a temporary file name if name argument is empty.', () => {
  146. const opts = {safeFileNames: false};
  147. const result = parseFileName(opts);
  148. assert.equal(typeof result, 'string');
  149. });
  150. });
  151. //buildOptions tests
  152. describe('Test buildOptions function', () => {
  153. const source = { option1: '1', option2: '2' };
  154. const sourceAddon = { option3: '3'};
  155. const expected = { option1: '1', option2: '2' };
  156. const expectedAddon = { option1: '1', option2: '2', option3: '3'};
  157. it('buildOptions returns and equal object to the object which was paased', () => {
  158. let result = buildOptions(source);
  159. assert.deepStrictEqual(result, source);
  160. });
  161. it('buildOptions doesnt add non object or null arguments to the result', () => {
  162. let result = buildOptions(source, 2, '3', null);
  163. assert.deepStrictEqual(result, expected);
  164. });
  165. it('buildOptions adds value to the result from the several source argumets', () => {
  166. let result = buildOptions(source, sourceAddon);
  167. assert.deepStrictEqual(result, expectedAddon);
  168. });
  169. });
  170. //buildFields tests
  171. describe('Test buildOptions function', () => {
  172. it('buildFields does nothing if null value has been passed', () => {
  173. let fields = null;
  174. fields = buildFields(fields, 'test', null);
  175. assert.equal(fields, null);
  176. });
  177. });
  178. //checkAndMakeDir tests
  179. describe('Test checkAndMakeDir function', () => {
  180. //
  181. it('checkAndMakeDir returns false if upload options object was not set', () => {
  182. assert.equal(checkAndMakeDir(), false);
  183. });
  184. //
  185. it('checkAndMakeDir returns false if upload option createParentPath was not set', () => {
  186. assert.equal(checkAndMakeDir({}), false);
  187. });
  188. //
  189. it('checkAndMakeDir returns false if filePath was not set', () => {
  190. assert.equal(checkAndMakeDir({createParentPath: true}), false);
  191. });
  192. //
  193. it('checkAndMakeDir return true if path to the file already exists', ()=>{
  194. let dir = path.join(uploadDir, 'testfile');
  195. assert.equal(checkAndMakeDir({createParentPath: true}, dir), true);
  196. });
  197. //
  198. it('checkAndMakeDir creates a dir if path to the file not exists', ()=>{
  199. let dir = path.join(uploadDir, 'testfolder', 'testfile');
  200. assert.equal(checkAndMakeDir({createParentPath: true}, dir), true);
  201. });
  202. //
  203. it('checkAndMakeDir creates a dir recursively if path to the file not exists', ()=>{
  204. let dir = path.join(uploadDir, 'testfolder', 'testsubfolder', 'testfile');
  205. assert.equal(checkAndMakeDir({createParentPath: true}, dir), true);
  206. });
  207. });
  208. //saveBufferToFile tests
  209. describe('Test saveBufferToFile function', function(){
  210. beforeEach(function() {
  211. server.clearUploadsDir();
  212. });
  213. it('Save buffer to a file', function(done) {
  214. let filePath = path.join(uploadDir, mockFile);
  215. saveBufferToFile(mockBuffer, filePath, function(err){
  216. if (err) {
  217. return done(err);
  218. }
  219. fs.stat(filePath, done);
  220. });
  221. });
  222. it('Failed if not a buffer passed', function(done) {
  223. let filePath = path.join(uploadDir, mockFile);
  224. saveBufferToFile(undefined, filePath, function(err){
  225. if (err) {
  226. return done();
  227. }
  228. });
  229. });
  230. it('Failed if wrong path passed', function(done) {
  231. let filePath = '';
  232. saveBufferToFile(mockFile, filePath, function(err){
  233. if (err) {
  234. return done();
  235. }
  236. });
  237. });
  238. });
  239. describe('Test deleteFile function', function(){
  240. beforeEach(function() {
  241. server.clearUploadsDir();
  242. });
  243. it('Failed if nonexistent file passed', function(done){
  244. let filePath = path.join(uploadDir, getTempFilename());
  245. deleteFile(filePath, function(err){
  246. if (err) {
  247. return done();
  248. }
  249. });
  250. });
  251. it('Delete a file', function(done){
  252. let srcPath = path.join(fileDir, mockFile);
  253. let dstPath = path.join(uploadDir, getTempFilename());
  254. //copy a file
  255. copyFile(srcPath, dstPath, function(err){
  256. if (err) {
  257. return done(err);
  258. }
  259. fs.stat(dstPath, (err)=>{
  260. if (err){
  261. return done(err);
  262. }
  263. // delete a file
  264. deleteFile(dstPath, function(err){
  265. if (err) {
  266. return done(err);
  267. }
  268. fs.stat(dstPath, (err)=>{
  269. if (err){
  270. return done();
  271. }
  272. //error if a file still exist
  273. done(err);
  274. });
  275. });
  276. });
  277. });
  278. });
  279. });
  280. describe('Test copyFile function', function(){
  281. beforeEach(function() {
  282. server.clearUploadsDir();
  283. });
  284. it('Copy a file and check a hash', function(done) {
  285. let srcPath = path.join(fileDir, mockFile);
  286. let dstPath = path.join(uploadDir, mockFile);
  287. copyFile(srcPath, dstPath, function(err){
  288. if (err) {
  289. return done(err);
  290. }
  291. fs.stat(dstPath, (err)=>{
  292. if (err){
  293. return done(err);
  294. }
  295. //Match source and destination files hash.
  296. let fileBuffer = fs.readFileSync(dstPath);
  297. let fileHash = md5(fileBuffer);
  298. return (fileHash === mockHash) ? done() : done(err);
  299. });
  300. });
  301. });
  302. it('Failed if wrong source file path passed', function(done){
  303. let srcPath = path.join(fileDir, 'unknown');
  304. let dstPath = path.join(uploadDir, mockFile);
  305. copyFile(srcPath, dstPath, function(err){
  306. if (err) {
  307. return done();
  308. }
  309. });
  310. });
  311. it('Failed if wrong destination file path passed', function(done){
  312. let srcPath = path.join(fileDir, 'unknown');
  313. let dstPath = path.join('unknown', 'unknown');
  314. copyFile(srcPath, dstPath, function(err){
  315. if (err) {
  316. return done();
  317. }
  318. });
  319. });
  320. });
  321. describe('Test uriDecodeFileName function', function() {
  322. const testData = [
  323. { enc: 'test%22filename', dec: 'test"filename' },
  324. { enc: 'test%60filename', dec: 'test`filename' },
  325. { enc: '%3Fx%3Dtest%22filename', dec: '?x=test"filename'}
  326. ];
  327. // Test decoding if uriDecodeFileNames: true.
  328. testData.forEach((testName) => {
  329. const opts = { uriDecodeFileNames: true };
  330. it(`Return ${testName.dec} for input ${testName.enc} if uriDecodeFileNames: true`, () => {
  331. assert.equal(uriDecodeFileName(opts, testName.enc), testName.dec);
  332. });
  333. });
  334. // Test decoding if uriDecodeFileNames: false.
  335. testData.forEach((testName) => {
  336. const opts = { uriDecodeFileNames: false };
  337. it(`Return ${testName.enc} for input ${testName.enc} if uriDecodeFileNames: false`, () => {
  338. assert.equal(uriDecodeFileName(opts, testName.enc), testName.enc);
  339. });
  340. });
  341. });
  342. describe('Test for no prototype pollution in buildFields', function() {
  343. const prototypeFields = [
  344. { name: '__proto__', data: {} },
  345. { name: 'constructor', data: {} },
  346. { name: 'toString', data: {} }
  347. ];
  348. const nonPrototypeFields = [
  349. { name: 'a', data: {} },
  350. { name: 'b', data: {} }
  351. ];
  352. let fieldObject = undefined;
  353. [...prototypeFields, ...nonPrototypeFields].forEach((field) => {
  354. fieldObject = buildFields(fieldObject, field.name, field.data);
  355. });
  356. it(`Has ${nonPrototypeFields.length} keys`, () => {
  357. assert.equal(Object.keys(fieldObject).length, nonPrototypeFields.length);
  358. });
  359. it(`Has null as its prototype`, () => {
  360. assert.equal(Object.getPrototypeOf(fieldObject), null);
  361. });
  362. prototypeFields.forEach((field) => {
  363. it(`${field.name} property is not an array`, () => {
  364. // Note, Array.isArray is an insufficient test due to it returning false
  365. // for Objects with an array prototype.
  366. assert.equal(fieldObject[field.name] instanceof Array, false);
  367. });
  368. });
  369. });
  370. describe('Test for correct detection of prototype pollution', function() {
  371. const validInsertions = [
  372. { base: {}, key: 'a' },
  373. { base: { a: 1 }, key: 'a' },
  374. { base: { __proto__: { a: 1 } }, key: 'a' },
  375. { base: [1], key: 0 },
  376. { base: { __proto__: [1] }, key: 0 }
  377. ];
  378. const invalidInsertions = [
  379. { base: {}, key: '__proto__' },
  380. { base: {}, key: 'constructor' },
  381. { base: [1], key: '__proto__' },
  382. { base: [1], key: 'length' },
  383. { base: { __proto__: [1] }, key: 'length' }
  384. ];
  385. validInsertions.forEach((insertion) => {
  386. it(`Key ${insertion.key} should be valid for ${JSON.stringify(insertion.base)}`, () => {
  387. assert.equal(isSafeFromPollution(insertion.base, insertion.key), true);
  388. });
  389. });
  390. invalidInsertions.forEach((insertion) => {
  391. it(`Key ${insertion.key} should not be valid for ${JSON.stringify(insertion.base)}`, () => {
  392. assert.equal(isSafeFromPollution(insertion.base, insertion.key), false);
  393. });
  394. });
  395. });
  396. });