fix: sanitize SVG uploads
This commit is contained in:
parent
79e153815f
commit
5d3e81496f
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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'])
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
25
server/jobs/sanitize-svg.js
Normal file
25
server/jobs/sanitize-svg.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user