From 43f8f8cadc6ff3657234d09feb8d7278d8b504f6 Mon Sep 17 00:00:00 2001 From: Cagdas U Date: Sun, 26 Jul 2020 14:27:58 +0300 Subject: [PATCH] fix(upload): add mime-type check at file uploads Add mime-type check at `UploadService#create`. Addresses topcoder-platform/u-bahn-app#269 --- .../src/components/Upload/Initial/index.jsx | 24 +++--- package-lock.json | 74 +++++++++++++++++++ package.json | 1 + src/services/UploadService.js | 25 +++++++ 4 files changed, 115 insertions(+), 9 deletions(-) diff --git a/client/src/components/Upload/Initial/index.jsx b/client/src/components/Upload/Initial/index.jsx index a446589..14887d9 100644 --- a/client/src/components/Upload/Initial/index.jsx +++ b/client/src/components/Upload/Initial/index.jsx @@ -40,14 +40,15 @@ export default function Initial({ onError, onUpload, templateId }) { }; const upload = (files) => { - const allowedMineTypes = [ - "application/vnd.ms-excel", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "text/csv", - ]; - if (files && files[0] && allowedMineTypes.indexOf(files[0].type) !== -1) - onUpload(files[0]); - else setInvalidFileExtension(true); + const allowedExtensions = ["xls", "xlsx", "csv"]; + if (files && files[0]) { + const ext = files[0].name.split(".").pop(); + if (allowedExtensions.includes(ext.toLowerCase())) { + onUpload(files[0]); + } else { + setInvalidFileExtension(true); + } + } }; let contentStyle = style.content; @@ -85,6 +86,7 @@ export default function Initial({ onError, onUpload, templateId }) { onChange={(e) => upload(e.target.files)} ref={fileInputRef} type="file" + accept=".xls,.xlsx,.csv" /> icon
@@ -99,7 +101,11 @@ export default function Initial({ onError, onUpload, templateId }) {
Supports XLS, XLSX or CSV file
-
+
Download Import Template (.XLSX)
diff --git a/package-lock.json b/package-lock.json index ad9b8d7..15f9307 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,6 +70,11 @@ "@hapi/hoek": "^8.3.0" } }, + "@tokenizer/token": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.1.1.tgz", + "integrity": "sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w==" + }, "@types/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", @@ -93,6 +98,11 @@ "@types/node": "*" } }, + "@types/debug": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", + "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==" + }, "@types/express": { "version": "4.17.7", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz", @@ -1577,6 +1587,17 @@ "flat-cache": "^2.0.1" } }, + "file-type": { + "version": "14.6.2", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-14.6.2.tgz", + "integrity": "sha512-kSZTAJxPXBdBgJyoC7TexkBWoMI/D1Gas6aTtAn9VIRFwCehwiluGV5O8O2GwqO5zIqeEvXxEKl/xfcaAKB0Yg==", + "requires": { + "readable-web-to-node-stream": "^2.0.0", + "strtok3": "^6.0.3", + "token-types": "^2.0.0", + "typedarray-to-buffer": "^3.1.5" + } + }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -2801,6 +2822,11 @@ "pify": "^2.0.0" } }, + "peek-readable": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-3.1.0.tgz", + "integrity": "sha512-KGuODSTV6hcgdZvDrIDBUkN0utcAVj1LL7FfGbM0viKTtCHmtZcuEJ+lGqsp0fTFkGqesdtemV2yUSMeyy3ddA==" + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -3072,6 +3098,11 @@ "util-deprecate": "^1.0.1" } }, + "readable-web-to-node-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-2.0.0.tgz", + "integrity": "sha512-+oZJurc4hXpaaqsN68GoZGQAQIA3qr09Or4fqEsargABnbe5Aau8hFn6ISVleT3cpY/0n/8drn7huyyEvTbghA==" + }, "reconnect-core": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/reconnect-core/-/reconnect-core-1.3.0.tgz", @@ -3548,6 +3579,32 @@ "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", "dev": true }, + "strtok3": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.0.3.tgz", + "integrity": "sha512-/3RaYN9rW5WEYNHSvn081CgL4HziT027hfi5tsksbPfeWxi3BSLb8tolZDzpYU3I78/0ZqRiFpMDAqN2t4YShA==", + "requires": { + "@tokenizer/token": "^0.1.1", + "@types/debug": "^4.1.5", + "debug": "^4.1.1", + "peek-readable": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "superagent": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", @@ -3750,6 +3807,15 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "token-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-2.0.0.tgz", + "integrity": "sha512-WWvu8sGK8/ZmGusekZJJ5NM6rRVTTDO7/bahz4NGiSDb/XsmdYBn6a1N/bymUHuWYTWeuLUg98wUzvE4jPdCZw==", + "requires": { + "@tokenizer/token": "^0.1.0", + "ieee754": "^1.1.13" + } + }, "topo": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", @@ -3840,6 +3906,14 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, "unfetch": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.1.0.tgz", diff --git a/package.json b/package.json index ccc5fba..22ba665 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "dynamoose": "^1.8.0", "express": "^4.17.1", "express-interceptor": "^1.2.0", + "file-type": "^14.6.2", "get-parameter-names": "^0.3.0", "http-status-codes": "^1.3.0", "js-yaml": "^3.14.0", diff --git a/src/services/UploadService.js b/src/services/UploadService.js index 05e52a4..d9dc270 100644 --- a/src/services/UploadService.js +++ b/src/services/UploadService.js @@ -5,9 +5,33 @@ const _ = require('lodash') const Joi = require('joi') const config = require('config') const { v4: uuid } = require('uuid') +const FileType = require('file-type') +const errors = require('../common/errors') const helper = require('../common/helper') const logger = require('../common/logger') +/** + * Checks the type of uploaded file and ensures it's allowed. + * @param {Object} upload The uploaded file + */ +async function ensureFileTypeIsValid(upload) { + const allowedExtensions = ['xls', 'xlsx', 'csv'] + const allowedMimeTypes = [ + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/csv', + ] + const fileType = await FileType.fromBuffer(upload.buffer) + const fileExt = upload.originalname.split('.').pop().toLowerCase() + + const isValidMimeType = fileType && _.includes(allowedMimeTypes, fileType.mime) + const isValidExt = _.includes(allowedExtensions, fileExt) + const isAllowed = fileType !== undefined ? isValidMimeType : isValidExt + if (isAllowed === false) { + throw new errors.ForbiddenError(`You are allowed to upload only ${_.join(allowedExtensions, ',')} types.`) + } +} + /** * Get upload entity by id. * @param {String} id the upload id @@ -31,6 +55,7 @@ getEntity.schema = { * @returns {Object} the created upload */ async function create (authUser, upload, data) { + await ensureFileTypeIsValid(upload) const id = uuid() // upload file to s3 under uploads folder const objectKey = await helper.uploadToS3(config.UPLOAD_S3_BUCKET, upload, `uploads/${id}`)