fix: sanitize SVG uploads

This commit is contained in:
NGPixel 2021-12-17 21:41:23 -05:00
parent 79e153815f
commit 5d3e81496f
No known key found for this signature in database
GPG Key ID: 8FDA2F1757F60D63
6 changed files with 79 additions and 25 deletions

View File

@ -142,6 +142,15 @@
:suffix='$t(`admin:security.maxUploadBatchSuffix`)' :suffix='$t(`admin:security.maxUploadBatchSuffix`)'
style='max-width: 450px;' 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-card.mt-3.animated.fadeInUp.wait-p2s
v-toolbar(flat, color='primary', dark, dense) v-toolbar(flat, color='primary', dark, dense)
@ -242,6 +251,7 @@ export default {
config: { config: {
uploadMaxFileSize: 0, uploadMaxFileSize: 0,
uploadMaxFiles: 0, uploadMaxFiles: 0,
uploadScanSVG: true,
securityOpenRedirect: true, securityOpenRedirect: true,
securityIframe: true, securityIframe: true,
securityReferrerPolicy: true, securityReferrerPolicy: true,
@ -286,6 +296,7 @@ export default {
$authJwtRenewablePeriod: String $authJwtRenewablePeriod: String
$uploadMaxFileSize: Int $uploadMaxFileSize: Int
$uploadMaxFiles: Int $uploadMaxFiles: Int
$uploadScanSVG: Boolean
$securityOpenRedirect: Boolean $securityOpenRedirect: Boolean
$securityIframe: Boolean $securityIframe: Boolean
$securityReferrerPolicy: Boolean $securityReferrerPolicy: Boolean
@ -307,6 +318,7 @@ export default {
authJwtRenewablePeriod: $authJwtRenewablePeriod, authJwtRenewablePeriod: $authJwtRenewablePeriod,
uploadMaxFileSize: $uploadMaxFileSize, uploadMaxFileSize: $uploadMaxFileSize,
uploadMaxFiles: $uploadMaxFiles, uploadMaxFiles: $uploadMaxFiles,
uploadScanSVG: $uploadScanSVG
securityOpenRedirect: $securityOpenRedirect, securityOpenRedirect: $securityOpenRedirect,
securityIframe: $securityIframe, securityIframe: $securityIframe,
securityReferrerPolicy: $securityReferrerPolicy, securityReferrerPolicy: $securityReferrerPolicy,
@ -337,6 +349,7 @@ export default {
authJwtRenewablePeriod: _.get(this.config, 'authJwtRenewablePeriod', ''), authJwtRenewablePeriod: _.get(this.config, 'authJwtRenewablePeriod', ''),
uploadMaxFileSize: _.toSafeInteger(_.get(this.config, 'uploadMaxFileSize', 0)), uploadMaxFileSize: _.toSafeInteger(_.get(this.config, 'uploadMaxFileSize', 0)),
uploadMaxFiles: _.toSafeInteger(_.get(this.config, 'uploadMaxFiles', 0)), uploadMaxFiles: _.toSafeInteger(_.get(this.config, 'uploadMaxFiles', 0)),
uploadScanSVG: _.get(this.config, 'uploadScanSVG', false),
securityOpenRedirect: _.get(this.config, 'securityOpenRedirect', false), securityOpenRedirect: _.get(this.config, 'securityOpenRedirect', false),
securityIframe: _.get(this.config, 'securityIframe', false), securityIframe: _.get(this.config, 'securityIframe', false),
securityReferrerPolicy: _.get(this.config, 'securityReferrerPolicy', false), securityReferrerPolicy: _.get(this.config, 'securityReferrerPolicy', false),
@ -388,6 +401,7 @@ export default {
authJwtRenewablePeriod authJwtRenewablePeriod
uploadMaxFileSize uploadMaxFileSize
uploadMaxFiles uploadMaxFiles
uploadScanSVG
securityOpenRedirect securityOpenRedirect
securityIframe securityIframe
securityReferrerPolicy securityReferrerPolicy

View File

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

View File

@ -29,7 +29,8 @@ module.exports = {
authJwtExpiration: WIKI.config.auth.tokenExpiration, authJwtExpiration: WIKI.config.auth.tokenExpiration,
authJwtRenewablePeriod: WIKI.config.auth.tokenRenewal, authJwtRenewablePeriod: WIKI.config.auth.tokenRenewal,
uploadMaxFileSize: WIKI.config.uploads.maxFileSize, 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 = { WIKI.config.uploads = {
maxFileSize: _.get(args, 'uploadMaxFileSize', WIKI.config.uploads.maxFileSize), 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']) await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'seo', 'logoUrl', 'auth', 'features', 'security', 'uploads'])

View File

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

View File

@ -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
}
}

View File

@ -99,6 +99,16 @@ module.exports = class Asset extends Model {
folderId: opts.folderId 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 // Save asset data
try { try {
const fileBuffer = await fs.readFile(opts.path) const fileBuffer = await fs.readFile(opts.path)