瀏覽代碼

fix: sanitize SVG uploads

NGPixel 3 年之前
父節點
當前提交
5d3e81496f

+ 14 - 0
client/components/admin/admin-security.vue

@@ -142,6 +142,15 @@
                     :suffix='$t(`admin:security.maxUploadBatchSuffix`)'
                     style='max-width: 450px;'
                     )
+                  v-divider.mt-3
+                  v-switch(
+                    inset
+                    label='Scan and Sanitize SVG Uploads'
+                    color='primary'
+                    v-model='config.uploadScanSVG'
+                    persistent-hint
+                    hint='Should SVG uploads be scanned for vulnerabilities and stripped of any potentially unsafe content.'
+                    )
 
               v-card.mt-3.animated.fadeInUp.wait-p2s
                 v-toolbar(flat, color='primary', dark, dense)
@@ -242,6 +251,7 @@ export default {
       config: {
         uploadMaxFileSize: 0,
         uploadMaxFiles: 0,
+        uploadScanSVG: true,
         securityOpenRedirect: true,
         securityIframe: true,
         securityReferrerPolicy: true,
@@ -286,6 +296,7 @@ export default {
               $authJwtRenewablePeriod: String
               $uploadMaxFileSize: Int
               $uploadMaxFiles: Int
+              $uploadScanSVG: Boolean
               $securityOpenRedirect: Boolean
               $securityIframe: Boolean
               $securityReferrerPolicy: Boolean
@@ -307,6 +318,7 @@ export default {
                   authJwtRenewablePeriod: $authJwtRenewablePeriod,
                   uploadMaxFileSize: $uploadMaxFileSize,
                   uploadMaxFiles: $uploadMaxFiles,
+                  uploadScanSVG: $uploadScanSVG
                   securityOpenRedirect: $securityOpenRedirect,
                   securityIframe: $securityIframe,
                   securityReferrerPolicy: $securityReferrerPolicy,
@@ -337,6 +349,7 @@ export default {
             authJwtRenewablePeriod: _.get(this.config, 'authJwtRenewablePeriod', ''),
             uploadMaxFileSize: _.toSafeInteger(_.get(this.config, 'uploadMaxFileSize', 0)),
             uploadMaxFiles: _.toSafeInteger(_.get(this.config, 'uploadMaxFiles', 0)),
+            uploadScanSVG: _.get(this.config, 'uploadScanSVG', false),
             securityOpenRedirect: _.get(this.config, 'securityOpenRedirect', false),
             securityIframe: _.get(this.config, 'securityIframe', false),
             securityReferrerPolicy: _.get(this.config, 'securityReferrerPolicy', false),
@@ -388,6 +401,7 @@ export default {
               authJwtRenewablePeriod
               uploadMaxFileSize
               uploadMaxFiles
+              uploadScanSVG
               securityOpenRedirect
               securityIframe
               securityReferrerPolicy

+ 1 - 0
server/app/data.yml

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

+ 4 - 2
server/graph/resolvers/site.js

@@ -29,7 +29,8 @@ module.exports = {
         authJwtExpiration: WIKI.config.auth.tokenExpiration,
         authJwtRenewablePeriod: WIKI.config.auth.tokenRenewal,
         uploadMaxFileSize: WIKI.config.uploads.maxFileSize,
-        uploadMaxFiles: WIKI.config.uploads.maxFiles
+        uploadMaxFiles: WIKI.config.uploads.maxFiles,
+        uploadScanSVG: WIKI.config.uploads.scanSVG
       }
     }
   },
@@ -97,7 +98,8 @@ module.exports = {
 
         WIKI.config.uploads = {
           maxFileSize: _.get(args, 'uploadMaxFileSize', WIKI.config.uploads.maxFileSize),
-          maxFiles: _.get(args, 'uploadMaxFiles', WIKI.config.uploads.maxFiles)
+          maxFiles: _.get(args, 'uploadMaxFiles', WIKI.config.uploads.maxFiles),
+          scanSVG: _.get(args, 'uploadScanSVG', WIKI.config.uploads.scanSVG)
         }
 
         await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'seo', 'logoUrl', 'auth', 'features', 'security', 'uploads'])

+ 25 - 23
server/graph/schemas/site.graphql

@@ -54,6 +54,7 @@ type SiteMutation {
     securityCSPDirectives: String
     uploadMaxFileSize: Int
     uploadMaxFiles: Int
+    uploadScanSVG: Boolean
 
   ): DefaultResponse @auth(requires: ["manage:system"])
 }
@@ -63,15 +64,15 @@ type SiteMutation {
 # -----------------------------------------------
 
 type SiteConfig {
-  host: String!
-  title: String!
-  description: String!
-  robots: [String]!
-  analyticsService: String!
-  analyticsId: String!
-  company: String!
-  contentLicense: String!
-  logoUrl: String!
+  host: String
+  title: String
+  description: String
+  robots: [String]
+  analyticsService: String
+  analyticsId: String
+  company: String
+  contentLicense: String
+  logoUrl: String
   authAutoLogin: Boolean
   authEnforce2FA: Boolean
   authHideLocal: Boolean
@@ -79,18 +80,19 @@ type SiteConfig {
   authJwtAudience: String
   authJwtExpiration: String
   authJwtRenewablePeriod: String
-  featurePageRatings: Boolean!
-  featurePageComments: Boolean!
-  featurePersonalWikis: Boolean!
-  securityOpenRedirect: Boolean!
-  securityIframe: Boolean!
-  securityReferrerPolicy: Boolean!
-  securityTrustProxy: Boolean!
-  securitySRI: Boolean!
-  securityHSTS: Boolean!
-  securityHSTSDuration: Int!
-  securityCSP: Boolean!
-  securityCSPDirectives: String!
-  uploadMaxFileSize: Int!
-  uploadMaxFiles: Int!
+  featurePageRatings: Boolean
+  featurePageComments: Boolean
+  featurePersonalWikis: Boolean
+  securityOpenRedirect: Boolean
+  securityIframe: Boolean
+  securityReferrerPolicy: Boolean
+  securityTrustProxy: Boolean
+  securitySRI: Boolean
+  securityHSTS: Boolean
+  securityHSTSDuration: Int
+  securityCSP: Boolean
+  securityCSPDirectives: String
+  uploadMaxFileSize: Int
+  uploadMaxFiles: Int
+  uploadScanSVG: Boolean
 }

+ 25 - 0
server/jobs/sanitize-svg.js

@@ -0,0 +1,25 @@
+const fs = require('fs-extra')
+const { JSDOM } = require('jsdom')
+const createDOMPurify = require('dompurify')
+
+/* global WIKI */
+
+module.exports = async (svgPath) => {
+  WIKI.logger.info(`Sanitizing SVG file upload...`)
+
+  try {
+    let svgContents = await fs.readFile(svgPath, 'utf8')
+
+    const window = new JSDOM('').window
+    const DOMPurify = createDOMPurify(window)
+
+    svgContents = DOMPurify.sanitize(svgContents)
+
+    await fs.writeFile(svgPath, svgContents)
+    WIKI.logger.info(`Sanitized SVG file upload: [ COMPLETED ]`)
+  } catch (err) {
+    WIKI.logger.error(`Failed to sanitize SVG file upload: [ FAILED ]`)
+    WIKI.logger.error(err.message)
+    throw err
+  }
+}

+ 10 - 0
server/models/assets.js

@@ -99,6 +99,16 @@ module.exports = class Asset extends Model {
       folderId: opts.folderId
     }
 
+    // Sanitize SVG contents
+    if (WIKI.config.uploads.scanSVG && opts.mimetype === 'image/svg+xml') {
+      const svgSanitizeJob = await WIKI.scheduler.registerJob({
+        name: 'sanitize-svg',
+        immediate: true,
+        worker: true
+      }, opts.path)
+      await svgSanitizeJob.finished
+    }
+
     // Save asset data
     try {
       const fileBuffer = await fs.readFile(opts.path)