From 5d3e81496fba1f0fbd64eeb855f30f69a9040718 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Fri, 17 Dec 2021 21:41:23 -0500 Subject: [PATCH] fix: sanitize SVG uploads --- client/components/admin/admin-security.vue | 14 +++++++ server/app/data.yml | 1 + server/graph/resolvers/site.js | 6 ++- server/graph/schemas/site.graphql | 48 +++++++++++----------- server/jobs/sanitize-svg.js | 25 +++++++++++ server/models/assets.js | 10 +++++ 6 files changed, 79 insertions(+), 25 deletions(-) create mode 100644 server/jobs/sanitize-svg.js diff --git a/client/components/admin/admin-security.vue b/client/components/admin/admin-security.vue index d2450039..ce9c2583 100644 --- a/client/components/admin/admin-security.vue +++ b/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 diff --git a/server/app/data.yml b/server/app/data.yml index 60f308f5..cb9f2bf6 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -80,6 +80,7 @@ defaults: uploads: maxFileSize: 5242880 maxFiles: 10 + scanSVG: true flags: ldapdebug: false sqllog: false diff --git a/server/graph/resolvers/site.js b/server/graph/resolvers/site.js index 7b7d4119..161719fb 100644 --- a/server/graph/resolvers/site.js +++ b/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']) diff --git a/server/graph/schemas/site.graphql b/server/graph/schemas/site.graphql index fcd68f50..65875ac5 100644 --- a/server/graph/schemas/site.graphql +++ b/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 } diff --git a/server/jobs/sanitize-svg.js b/server/jobs/sanitize-svg.js new file mode 100644 index 00000000..117c20e4 --- /dev/null +++ b/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 + } +} diff --git a/server/models/assets.js b/server/models/assets.js index 8b12060f..8548319f 100644 --- a/server/models/assets.js +++ b/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)