From 79bdd4409316adf649806de3e22352297f85cee0 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Fri, 24 Dec 2021 20:18:12 -0500 Subject: [PATCH] fix: force download of unsafe extensions --- client/components/admin/admin-security.vue | 14 ++++++++++++++ server/app/data.yml | 1 + server/graph/resolvers/site.js | 6 ++++-- server/graph/schemas/site.graphql | 2 ++ server/helpers/asset.js | 5 +++++ server/models/assets.js | 7 +++++++ 6 files changed, 33 insertions(+), 2 deletions(-) diff --git a/client/components/admin/admin-security.vue b/client/components/admin/admin-security.vue index ce9c25838c..7a8d305b2a 100644 --- a/client/components/admin/admin-security.vue +++ b/client/components/admin/admin-security.vue @@ -151,6 +151,15 @@ persistent-hint hint='Should SVG uploads be scanned for vulnerabilities and stripped of any potentially unsafe content.' ) + v-divider.mt-3 + v-switch( + inset + label='Force Download of Unsafe Extensions' + color='primary' + v-model='config.uploadForceDownload' + persistent-hint + hint='Should non-image files be forced as downloads when accessed directly. This prevents potential XSS attacks via unsafe file extensions uploads.' + ) v-card.mt-3.animated.fadeInUp.wait-p2s v-toolbar(flat, color='primary', dark, dense) @@ -252,6 +261,7 @@ export default { uploadMaxFileSize: 0, uploadMaxFiles: 0, uploadScanSVG: true, + uploadForceDownload: true, securityOpenRedirect: true, securityIframe: true, securityReferrerPolicy: true, @@ -297,6 +307,7 @@ export default { $uploadMaxFileSize: Int $uploadMaxFiles: Int $uploadScanSVG: Boolean + $uploadForceDownload: Boolean $securityOpenRedirect: Boolean $securityIframe: Boolean $securityReferrerPolicy: Boolean @@ -319,6 +330,7 @@ export default { uploadMaxFileSize: $uploadMaxFileSize, uploadMaxFiles: $uploadMaxFiles, uploadScanSVG: $uploadScanSVG + uploadForceDownload: $uploadForceDownload, securityOpenRedirect: $securityOpenRedirect, securityIframe: $securityIframe, securityReferrerPolicy: $securityReferrerPolicy, @@ -350,6 +362,7 @@ export default { uploadMaxFileSize: _.toSafeInteger(_.get(this.config, 'uploadMaxFileSize', 0)), uploadMaxFiles: _.toSafeInteger(_.get(this.config, 'uploadMaxFiles', 0)), uploadScanSVG: _.get(this.config, 'uploadScanSVG', false), + uploadForceDownload: _.get(this.config, 'uploadForceDownload', false), securityOpenRedirect: _.get(this.config, 'securityOpenRedirect', false), securityIframe: _.get(this.config, 'securityIframe', false), securityReferrerPolicy: _.get(this.config, 'securityReferrerPolicy', false), @@ -402,6 +415,7 @@ export default { uploadMaxFileSize uploadMaxFiles uploadScanSVG + uploadForceDownload securityOpenRedirect securityIframe securityReferrerPolicy diff --git a/server/app/data.yml b/server/app/data.yml index cb9f2bf6cd..0e5e45e218 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -81,6 +81,7 @@ defaults: maxFileSize: 5242880 maxFiles: 10 scanSVG: true + forceDownload: true flags: ldapdebug: false sqllog: false diff --git a/server/graph/resolvers/site.js b/server/graph/resolvers/site.js index 161719fbf0..c325706c90 100644 --- a/server/graph/resolvers/site.js +++ b/server/graph/resolvers/site.js @@ -30,7 +30,8 @@ module.exports = { authJwtRenewablePeriod: WIKI.config.auth.tokenRenewal, uploadMaxFileSize: WIKI.config.uploads.maxFileSize, uploadMaxFiles: WIKI.config.uploads.maxFiles, - uploadScanSVG: WIKI.config.uploads.scanSVG + uploadScanSVG: WIKI.config.uploads.scanSVG, + uploadForceDownload: WIKI.config.uploads.forceDownload } } }, @@ -99,7 +100,8 @@ module.exports = { WIKI.config.uploads = { maxFileSize: _.get(args, 'uploadMaxFileSize', WIKI.config.uploads.maxFileSize), maxFiles: _.get(args, 'uploadMaxFiles', WIKI.config.uploads.maxFiles), - scanSVG: _.get(args, 'uploadScanSVG', WIKI.config.uploads.scanSVG) + scanSVG: _.get(args, 'uploadScanSVG', WIKI.config.uploads.scanSVG), + forceDownload: _.get(args, 'uploadForceDownload', WIKI.config.uploads.forceDownload) } await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'seo', 'logoUrl', 'auth', 'features', 'security', 'uploads']) diff --git a/server/graph/schemas/site.graphql b/server/graph/schemas/site.graphql index 65875ac56a..5544fffe8c 100644 --- a/server/graph/schemas/site.graphql +++ b/server/graph/schemas/site.graphql @@ -55,6 +55,7 @@ type SiteMutation { uploadMaxFileSize: Int uploadMaxFiles: Int uploadScanSVG: Boolean + uploadForceDownload: Boolean ): DefaultResponse @auth(requires: ["manage:system"]) } @@ -95,4 +96,5 @@ type SiteConfig { uploadMaxFileSize: Int uploadMaxFiles: Int uploadScanSVG: Boolean + uploadForceDownload: Boolean } diff --git a/server/helpers/asset.js b/server/helpers/asset.js index 56f76e0d9f..95b202db49 100644 --- a/server/helpers/asset.js +++ b/server/helpers/asset.js @@ -1,4 +1,5 @@ const crypto = require('crypto') +const path = require('path') module.exports = { /** @@ -6,5 +7,9 @@ module.exports = { */ generateHash(assetPath) { return crypto.createHash('sha1').update(assetPath).digest('hex') + }, + + getPathInfo(assetPath) { + return path.parse(assetPath.toLowerCase()) } } diff --git a/server/models/assets.js b/server/models/assets.js index 9d0a79b1c7..94cb4a0e83 100644 --- a/server/models/assets.js +++ b/server/models/assets.js @@ -168,8 +168,15 @@ module.exports = class Asset extends Model { static async getAsset(assetPath, res) { try { + const fileInfo = assetHelper.getPathInfo(assetPath) const fileHash = assetHelper.generateHash(assetPath) const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`) + + // Force unsafe extensions to download + if (WIKI.config.uploads.forceDownload && !['.png', '.apng', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg'].includes(fileInfo.ext)) { + res.set('Content-disposition', 'attachment; filename=' + fileInfo.base) + } + if (await WIKI.models.assets.getAssetFromCache(assetPath, cachePath, res)) { return }