diff --git a/client/components/admin.vue b/client/components/admin.vue index 4decb7ba..92684c7a 100644 --- a/client/components/admin.vue +++ b/client/components/admin.vue @@ -89,6 +89,9 @@ v-list-item(to='/mail', color='primary', v-if='hasPermission(`manage:system`)') v-list-item-avatar(size='24', tile): v-icon mdi-email-multiple-outline v-list-item-title {{ $t('admin:mail.title') }} + v-list-item(to='/security', v-if='hasPermission(`manage:system`)') + v-list-item-avatar(size='24', tile): v-icon mdi-lock-check + v-list-item-title {{ $t('admin:security.title') }} v-list-item(to='/ssl', v-if='hasPermission(`manage:system`)') v-list-item-avatar(size='24', tile): v-icon mdi-cloud-lock-outline v-list-item-title {{ $t('admin:ssl.title') }} @@ -172,6 +175,7 @@ const router = new VueRouter({ { path: '/storage', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-storage.vue') }, { path: '/api', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-api.vue') }, { path: '/mail', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-mail.vue') }, + { path: '/security', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-security.vue') }, { path: '/ssl', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-ssl.vue') }, { path: '/system', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-system.vue') }, { path: '/utilities', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-utilities.vue') }, diff --git a/client/components/admin/admin-general.vue b/client/components/admin/admin-general.vue index 75a41ab6..02507658 100644 --- a/client/components/admin/admin-general.vue +++ b/client/components/admin/admin-general.vue @@ -167,93 +167,6 @@ disabled ) - v-card.mt-5.animated.fadeInUp.wait-p5s - v-toolbar(color='red darken-2', dark, dense, flat) - v-toolbar-title.subtitle-1 Security - v-card-text - v-alert(outlined, color='red darken-2', icon='mdi-information-outline').body-2 Make sure to understand the implications before turning on / off a security feature. - v-switch.mt-3( - inset - label='Block IFrame Embedding' - color='red darken-2' - v-model='config.securityIframe' - persistent-hint - hint='Prevents other websites from embedding your wiki in an iframe. This provides clickjacking protection.' - ) - - v-divider.mt-3 - v-switch( - inset - label='Same Origin Referrer Policy' - color='red darken-2' - v-model='config.securityReferrerPolicy' - persistent-hint - hint='Limits the referrer header to same origin.' - ) - - v-divider.mt-3 - v-switch( - inset - label='Trust X-Forwarded-* Proxy Headers' - color='red darken-2' - v-model='config.securityTrustProxy' - persistent-hint - hint='Should be enabled when using a reverse-proxy like nginx, apache, CloudFlare, etc in front of Wiki.js. Turn off otherwise.' - ) - - v-divider.mt-3 - v-switch( - inset - label='Subresource Integrity (SRI)' - color='red darken-2' - v-model='config.securitySRI' - persistent-hint - hint='This ensure that resources such as CSS and JS files are not altered during delivery.' - disabled - ) - - v-divider.mt-3 - v-switch( - inset - label='Enforce HSTS' - color='red darken-2' - v-model='config.securityHSTS' - persistent-hint - hint='This ensures the connection cannot be established through an insecure HTTP connection.' - ) - v-select.mt-5( - outlined - label='HSTS Max Age' - :items='hstsDurations' - v-model='config.securityHSTSDuration' - prepend-icon='mdi-subdirectory-arrow-right' - :disabled='!config.securityHSTS' - hide-details - style='max-width: 450px;' - ) - .pl-11.mt-3 - .caption Defines the duration for which the server should only deliver content through HTTPS. - .caption It's a good idea to start with small values and make sure that nothing breaks on your wiki before moving to longer values. - - v-divider.mt-3 - v-switch( - inset - label='Enforce CSP' - color='red darken-2' - v-model='config.securityCSP' - persistent-hint - hint='Restricts scripts to pre-approved content sources.' - disabled - ) - v-textarea.mt-5( - label='CSP Directives' - outlined - v-model='config.securityCSPDirectives' - prepend-icon='mdi-subdirectory-arrow-right' - persistent-hint - hint='One directive per line.' - disabled - ) component(:is='activeModal') @@ -296,24 +209,8 @@ export default { featurePageRatings: false, featurePageComments: false, featurePersonalWikis: false, - featureTinyPNG: false, - securityIframe: true, - securityReferrerPolicy: true, - securityTrustProxy: true, - securitySRI: true, - securityHSTS: false, - securityHSTSDuration: 0, - securityCSP: false, - securityCSPDirectives: '' + featureTinyPNG: false }, - hstsDurations: [ - { value: 300, text: '5 minutes' }, - { value: 86400, text: '1 day' }, - { value: 604800, text: '1 week' }, - { value: 2592000, text: '1 month' }, - { value: 31536000, text: '1 year' }, - { value: 63072000, text: '2 years' } - ], metaRobots: [ { text: 'Index', value: 'index' }, { text: 'Follow', value: 'follow' }, @@ -360,14 +257,6 @@ export default { $featurePageRatings: Boolean! $featurePageComments: Boolean! $featurePersonalWikis: Boolean! - $securityIframe: Boolean! - $securityReferrerPolicy: Boolean! - $securityTrustProxy: Boolean! - $securitySRI: Boolean! - $securityHSTS: Boolean! - $securityHSTSDuration: Int! - $securityCSP: Boolean! - $securityCSPDirectives: String! ) { site { updateConfig( @@ -382,15 +271,7 @@ export default { logoUrl: $logoUrl, featurePageRatings: $featurePageRatings, featurePageComments: $featurePageComments, - featurePersonalWikis: $featurePersonalWikis, - securityIframe: $securityIframe, - securityReferrerPolicy: $securityReferrerPolicy, - securityTrustProxy: $securityTrustProxy, - securitySRI: $securitySRI, - securityHSTS: $securityHSTS, - securityHSTSDuration: $securityHSTSDuration, - securityCSP: $securityCSP, - securityCSPDirectives: $securityCSPDirectives + featurePersonalWikis: $featurePersonalWikis ) { responseResult { succeeded @@ -414,15 +295,7 @@ export default { logoUrl: _.get(this.config, 'logoUrl', ''), featurePageRatings: _.get(this.config, 'featurePageRatings', false), featurePageComments: _.get(this.config, 'featurePageComments', false), - featurePersonalWikis: _.get(this.config, 'featurePersonalWikis', false), - securityIframe: _.get(this.config, 'securityIframe', false), - securityReferrerPolicy: _.get(this.config, 'securityReferrerPolicy', false), - securityTrustProxy: _.get(this.config, 'securityTrustProxy', false), - securitySRI: _.get(this.config, 'securitySRI', false), - securityHSTS: _.get(this.config, 'securityHSTS', false), - securityHSTSDuration: _.get(this.config, 'securityHSTSDuration', 0), - securityCSP: _.get(this.config, 'securityCSP', false), - securityCSPDirectives: _.get(this.config, 'securityCSPDirectives', '') + featurePersonalWikis: _.get(this.config, 'featurePersonalWikis', false) }, watchLoading (isLoading) { this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update') @@ -475,14 +348,6 @@ export default { featurePageRatings featurePageComments featurePersonalWikis - securityIframe - securityReferrerPolicy - securityTrustProxy - securitySRI - securityHSTS - securityHSTSDuration - securityCSP - securityCSPDirectives } } } diff --git a/client/components/admin/admin-security.vue b/client/components/admin/admin-security.vue new file mode 100644 index 00000000..649e13dd --- /dev/null +++ b/client/components/admin/admin-security.vue @@ -0,0 +1,265 @@ + + + + + diff --git a/client/static/svg/icon-private.svg b/client/static/svg/icon-private.svg new file mode 100644 index 00000000..1a716e7d --- /dev/null +++ b/client/static/svg/icon-private.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/config.sample.yml b/config.sample.yml index 73f208ff..c149f702 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -105,18 +105,6 @@ bindIP: 0.0.0.0 logLevel: info -# --------------------------------------------------------------------- -# Upload Limits -# --------------------------------------------------------------------- -# If you're using a reverse-proxy in front of Wiki.js, you must also -# change your proxy upload limits! - -uploads: - # Maximum upload size in bytes per file (default: 5242880 (5 MB)) - maxFileSize: 5242880 - # Maximum file uploads per request (default: 10) - maxFiles: 10 - # --------------------------------------------------------------------- # Offline Mode # --------------------------------------------------------------------- diff --git a/server/app/data.yml b/server/app/data.yml index 83b37e1a..6054110d 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -24,9 +24,6 @@ defaults: min: 1 bindIP: 0.0.0.0 logLevel: info - uploads: - maxFileSize: 5242880 - maxFiles: 10 offline: false ha: false # DB defaults @@ -67,6 +64,9 @@ defaults: securityCSPDirectives: '' server: sslRedir: false + uploads: + maxFileSize: 5242880 + maxFiles: 10 flags: ldapdebug: false sqllog: false diff --git a/server/controllers/upload.js b/server/controllers/upload.js index 65cd4d15..c6a3685d 100644 --- a/server/controllers/upload.js +++ b/server/controllers/upload.js @@ -10,13 +10,15 @@ const sanitize = require('sanitize-filename') /** * Upload files */ -router.post('/u', multer({ - dest: path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'), - limits: { - fileSize: WIKI.config.uploads.maxFileSize, - files: WIKI.config.uploads.maxFiles - } -}).array('mediaUpload'), async (req, res, next) => { +router.post('/u', (req, res, next) => { + multer({ + dest: path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'), + limits: { + fileSize: WIKI.config.uploads.maxFileSize, + files: WIKI.config.uploads.maxFiles + } + }).array('mediaUpload')(req, res, next) +}, async (req, res, next) => { if (!_.some(req.user.permissions, pm => _.includes(['write:assets', 'manage:system'], pm))) { return res.status(403).json({ succeeded: false, diff --git a/server/graph/resolvers/site.js b/server/graph/resolvers/site.js index bae94549..e939a247 100644 --- a/server/graph/resolvers/site.js +++ b/server/graph/resolvers/site.js @@ -20,44 +20,69 @@ module.exports = { logoUrl: WIKI.config.logoUrl, ...WIKI.config.seo, ...WIKI.config.features, - ...WIKI.config.security + ...WIKI.config.security, + uploadMaxFileSize: WIKI.config.uploads.maxFileSize, + uploadMaxFiles: WIKI.config.uploads.maxFiles } } }, SiteMutation: { async updateConfig(obj, args, context) { - let siteHost = _.trim(args.host) - if (siteHost.endsWith('/')) { - siteHost = siteHost.splice(0, -1) - } try { - WIKI.config.host = siteHost - WIKI.config.title = _.trim(args.title) - WIKI.config.company = _.trim(args.company) - WIKI.config.contentLicense = args.contentLicense + if (args.host) { + let siteHost = _.trim(args.host) + if (siteHost.endsWith('/')) { + siteHost = siteHost.splice(0, -1) + } + WIKI.config.host = siteHost + } + + if (args.title) { + WIKI.config.title = _.trim(args.title) + } + + if (args.company) { + WIKI.config.company = _.trim(args.company) + } + + if (args.contentLicense) { + WIKI.config.contentLicense = args.contentLicense + } + + if (args.logoUrl) { + WIKI.config.logoUrl = _.trim(args.logoUrl) + } + WIKI.config.seo = { - description: args.description, - robots: args.robots, - analyticsService: args.analyticsService, - analyticsId: args.analyticsId + description: _.get(args, 'description', WIKI.config.seo.description), + robots: _.get(args, 'robots', WIKI.config.seo.robots), + analyticsService: _.get(args, 'analyticsService', WIKI.config.seo.analyticsService), + analyticsId: _.get(args, 'analyticsId', WIKI.config.seo.analyticsId) } - WIKI.config.logoUrl = _.trim(args.logoUrl) + WIKI.config.features = { - featurePageRatings: args.featurePageRatings, - featurePageComments: args.featurePageComments, - featurePersonalWikis: args.featurePersonalWikis + featurePageRatings: _.get(args, 'featurePageRatings', WIKI.config.features.featurePageRatings), + featurePageComments: _.get(args, 'featurePageComments', WIKI.config.features.featurePageComments), + featurePersonalWikis: _.get(args, 'featurePersonalWikis', WIKI.config.features.featurePersonalWikis) } + WIKI.config.security = { - securityIframe: args.securityIframe, - securityReferrerPolicy: args.securityReferrerPolicy, - securityTrustProxy: args.securityTrustProxy, - securitySRI: args.securitySRI, - securityHSTS: args.securityHSTS, - securityHSTSDuration: args.securityHSTSDuration, - securityCSP: args.securityCSP, - securityCSPDirectives: args.securityCSPDirectives + securityIframe: _.get(args, 'securityIframe', WIKI.config.security.securityIframe), + securityReferrerPolicy: _.get(args, 'securityReferrerPolicy', WIKI.config.security.securityReferrerPolicy), + securityTrustProxy: _.get(args, 'securityTrustProxy', WIKI.config.security.securityTrustProxy), + securitySRI: _.get(args, 'securitySRI', WIKI.config.security.securitySRI), + securityHSTS: _.get(args, 'securityHSTS', WIKI.config.security.securityHSTS), + securityHSTSDuration: _.get(args, 'securityHSTSDuration', WIKI.config.security.securityHSTSDuration), + securityCSP: _.get(args, 'securityCSP', WIKI.config.security.securityCSP), + securityCSPDirectives: _.get(args, 'securityCSPDirectives', WIKI.config.security.securityCSPDirectives) + } + + WIKI.config.uploads = { + maxFileSize: _.get(args, 'uploadMaxFileSize', WIKI.config.uploads.maxFileSize), + maxFiles: _.get(args, 'uploadMaxFiles', WIKI.config.uploads.maxFiles) } - await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'seo', 'logoUrl', 'features', 'security']) + + await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'seo', 'logoUrl', 'features', 'security', 'uploads']) if (WIKI.config.security.securityTrustProxy) { WIKI.app.enable('trust proxy') diff --git a/server/graph/schemas/site.graphql b/server/graph/schemas/site.graphql index 90da127a..56f026be 100644 --- a/server/graph/schemas/site.graphql +++ b/server/graph/schemas/site.graphql @@ -24,26 +24,29 @@ type SiteQuery { type SiteMutation { updateConfig( - host: String! - title: String! - description: String! - robots: [String]! - analyticsService: String! - analyticsId: String! - company: String! - contentLicense: String! - logoUrl: String! - featurePageRatings: Boolean! - featurePageComments: Boolean! - featurePersonalWikis: Boolean! - securityIframe: Boolean! - securityReferrerPolicy: Boolean! - securityTrustProxy: Boolean! - securitySRI: Boolean! - securityHSTS: Boolean! - securityHSTSDuration: Int! - securityCSP: Boolean! - securityCSPDirectives: String! + host: String + title: String + description: String + robots: [String] + analyticsService: String + analyticsId: String + company: String + contentLicense: String + logoUrl: String + featurePageRatings: Boolean + featurePageComments: Boolean + featurePersonalWikis: Boolean + securityIframe: Boolean + securityReferrerPolicy: Boolean + securityTrustProxy: Boolean + securitySRI: Boolean + securityHSTS: Boolean + securityHSTSDuration: Int + securityCSP: Boolean + securityCSPDirectives: String + uploadMaxFileSize: Int + uploadMaxFiles: Int + ): DefaultResponse @auth(requires: ["manage:system"]) } @@ -72,4 +75,6 @@ type SiteConfig { securityHSTSDuration: Int! securityCSP: Boolean! securityCSPDirectives: String! + uploadMaxFileSize: Int! + uploadMaxFiles: Int! } diff --git a/server/setup.js b/server/setup.js index 550835d7..94bd8392 100644 --- a/server/setup.js +++ b/server/setup.js @@ -186,6 +186,7 @@ module.exports = () => { 'sessionSecret', 'telemetry', 'theming', + 'uploads', 'title' ], false)