2
0
Эх сурвалжийг харах

fix: force download of unsafe extensions

NGPixel 3 жил өмнө
parent
commit
79bdd44093

+ 14 - 0
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

+ 1 - 0
server/app/data.yml

@@ -81,6 +81,7 @@ defaults:
       maxFileSize: 5242880
       maxFiles: 10
       scanSVG: true
+      forceDownload: true
     flags:
       ldapdebug: false
       sqllog: false

+ 4 - 2
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'])

+ 2 - 0
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
 }

+ 5 - 0
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())
   }
 }

+ 7 - 0
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
       }