You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

444 lines
17 KiB

  1. <template lang='pug'>
  2. v-container(fluid, grid-list-lg)
  3. v-layout(row wrap)
  4. v-flex(xs12)
  5. .admin-header
  6. img.animated.fadeInUp(src='/_assets/svg/icon-private.svg', alt='Security', style='width: 80px;')
  7. .admin-header-title
  8. .headline.primary--text.animated.fadeInLeft {{ $t('admin:security.title') }}
  9. .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:security.subtitle') }}
  10. v-spacer
  11. v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)
  12. v-icon(left) mdi-check
  13. span {{$t('common:actions.apply')}}
  14. v-form.pt-3
  15. v-layout(row wrap)
  16. v-flex(lg6 xs12)
  17. v-card.animated.fadeInUp
  18. v-toolbar(color='red darken-2', dark, dense, flat)
  19. v-toolbar-title.subtitle-1 Security
  20. v-card-info(color='red')
  21. span Make sure to understand the implications before turning on / off a security feature.
  22. v-card-text
  23. v-switch(
  24. inset
  25. label='Block Open Redirect'
  26. color='red darken-2'
  27. v-model='config.securityOpenRedirect'
  28. persistent-hint
  29. hint='Prevents user controlled URLs from directing to websites outside of your wiki. This provides Open Redirect protection.'
  30. )
  31. v-divider.mt-3
  32. v-switch.mt-3(
  33. inset
  34. label='Block IFrame Embedding'
  35. color='red darken-2'
  36. v-model='config.securityIframe'
  37. persistent-hint
  38. hint='Prevents other websites from embedding your wiki in an iframe. This provides clickjacking protection.'
  39. )
  40. v-divider.mt-3
  41. v-switch(
  42. inset
  43. label='Same Origin Referrer Policy'
  44. color='red darken-2'
  45. v-model='config.securityReferrerPolicy'
  46. persistent-hint
  47. hint='Limits the referrer header to same origin.'
  48. )
  49. v-divider.mt-3
  50. v-switch(
  51. inset
  52. label='Trust X-Forwarded-* Proxy Headers'
  53. color='red darken-2'
  54. v-model='config.securityTrustProxy'
  55. persistent-hint
  56. hint='Should be enabled when using a reverse-proxy like nginx, apache, CloudFlare, etc in front of Wiki.js. Turn off otherwise.'
  57. )
  58. //- v-divider.mt-3
  59. //- v-switch(
  60. //- inset
  61. //- label='Subresource Integrity (SRI)'
  62. //- color='red darken-2'
  63. //- v-model='config.securitySRI'
  64. //- persistent-hint
  65. //- hint='This ensure that resources such as CSS and JS files are not altered during delivery.'
  66. //- disabled
  67. //- )
  68. v-divider.mt-3
  69. v-switch(
  70. inset
  71. label='Enforce HSTS'
  72. color='red darken-2'
  73. v-model='config.securityHSTS'
  74. persistent-hint
  75. hint='This ensures the connection cannot be established through an insecure HTTP connection.'
  76. )
  77. v-select.mt-5(
  78. outlined
  79. label='HSTS Max Age'
  80. :items='hstsDurations'
  81. v-model='config.securityHSTSDuration'
  82. prepend-icon='mdi-subdirectory-arrow-right'
  83. :disabled='!config.securityHSTS'
  84. hide-details
  85. style='max-width: 450px;'
  86. )
  87. .pl-11.mt-3
  88. .caption Defines the duration for which the server should only deliver content through HTTPS.
  89. .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.
  90. //- v-divider.mt-3
  91. //- v-switch(
  92. //- inset
  93. //- label='Enforce CSP'
  94. //- color='red darken-2'
  95. //- v-model='config.securityCSP'
  96. //- persistent-hint
  97. //- hint='Restricts scripts to pre-approved content sources.'
  98. //- disabled
  99. //- )
  100. //- v-textarea.mt-5(
  101. //- label='CSP Directives'
  102. //- outlined
  103. //- v-model='config.securityCSPDirectives'
  104. //- prepend-icon='mdi-subdirectory-arrow-right'
  105. //- persistent-hint
  106. //- hint='One directive per line.'
  107. //- disabled
  108. //- )
  109. v-flex(lg6 xs12)
  110. v-card.animated.fadeInUp.wait-p2s
  111. v-toolbar(color='primary', dark, dense, flat)
  112. v-toolbar-title.subtitle-1 {{ $t('admin:security.uploads') }}
  113. v-card-info(color='blue')
  114. span {{$t('admin:security.uploadsInfo')}}
  115. v-card-text
  116. v-text-field.mt-3(
  117. outlined
  118. :label='$t(`admin:security.maxUploadSize`)'
  119. required
  120. v-model='config.uploadMaxFileSize'
  121. prepend-icon='mdi-progress-upload'
  122. :hint='$t(`admin:security.maxUploadSizeHint`)'
  123. persistent-hint
  124. :suffix='$t(`admin:security.maxUploadSizeSuffix`)'
  125. style='max-width: 450px;'
  126. )
  127. v-text-field.mt-3(
  128. outlined
  129. :label='$t(`admin:security.maxUploadBatch`)'
  130. required
  131. v-model='config.uploadMaxFiles'
  132. prepend-icon='mdi-upload-lock'
  133. :hint='$t(`admin:security.maxUploadBatchHint`)'
  134. persistent-hint
  135. :suffix='$t(`admin:security.maxUploadBatchSuffix`)'
  136. style='max-width: 450px;'
  137. )
  138. v-divider.mt-3
  139. v-switch(
  140. inset
  141. label='Scan and Sanitize SVG Uploads'
  142. color='primary'
  143. v-model='config.uploadScanSVG'
  144. persistent-hint
  145. hint='Should SVG uploads be scanned for vulnerabilities and stripped of any potentially unsafe content.'
  146. )
  147. v-divider.mt-3
  148. v-switch(
  149. inset
  150. label='Force Download of Unsafe Extensions'
  151. color='primary'
  152. v-model='config.uploadForceDownload'
  153. persistent-hint
  154. hint='Should non-image files be forced as downloads when accessed directly. This prevents potential XSS attacks via unsafe file extensions uploads.'
  155. )
  156. v-card.mt-3.animated.fadeInUp.wait-p2s
  157. v-toolbar(flat, color='primary', dark, dense)
  158. .subtitle-1 {{$t('admin:security.login')}}
  159. //- v-card-info(color='blue')
  160. //- span {{$t('admin:security.loginInfo')}}
  161. .overline.grey--text.pa-4 {{$t('admin:security.loginScreen')}}
  162. .px-4.pb-3
  163. v-text-field(
  164. outlined
  165. :label='$t(`admin:security.loginBgUrl`)'
  166. v-model='config.authLoginBgUrl'
  167. :hint='$t(`admin:security.loginBgUrlHint`)'
  168. persistent-hint
  169. prepend-icon='mdi-image-area'
  170. append-icon='mdi-folder-image'
  171. @click:append='browseLoginBg'
  172. )
  173. v-switch(
  174. inset
  175. :label='$t(`admin:security.bypassLogin`)'
  176. color='primary'
  177. v-model='config.authAutoLogin'
  178. prepend-icon='mdi-fast-forward'
  179. persistent-hint
  180. :hint='$t(`admin:security.bypassLoginHint`)'
  181. )
  182. v-switch(
  183. inset
  184. :label='$t(`admin:security.hideLocalLogin`)'
  185. color='primary'
  186. v-model='config.authHideLocal'
  187. prepend-icon='mdi-eye-off-outline'
  188. persistent-hint
  189. :hint='$t(`admin:security.hideLocalLoginHint`)'
  190. )
  191. v-divider.mt-3
  192. .overline.grey--text.pa-4 {{$t('admin:security.loginSecurity')}}
  193. .px-4.pb-3
  194. v-switch.mt-0(
  195. inset
  196. :label='$t(`admin:security.enforce2fa`)'
  197. color='primary'
  198. v-model='config.authEnforce2FA'
  199. prepend-icon='mdi-two-factor-authentication'
  200. :hint='$t(`admin:security.enforce2faHint`)'
  201. persistent-hint
  202. )
  203. v-divider.mt-3
  204. .overline.grey--text.pa-4 {{$t('admin:security.jwt')}}
  205. .px-4.pb-3
  206. v-text-field(
  207. v-model='config.authJwtAudience'
  208. outlined
  209. prepend-icon='mdi-account-group-outline'
  210. :label='$t(`admin:auth.jwtAudience`)'
  211. :hint='$t(`admin:auth.jwtAudienceHint`)'
  212. persistent-hint
  213. )
  214. v-text-field.mt-3(
  215. v-model='config.authJwtExpiration'
  216. outlined
  217. prepend-icon='mdi-clock-outline'
  218. :label='$t(`admin:auth.tokenExpiration`)'
  219. :hint='$t(`admin:auth.tokenExpirationHint`)'
  220. persistent-hint
  221. )
  222. v-text-field.mt-3(
  223. v-model='config.authJwtRenewablePeriod'
  224. outlined
  225. prepend-icon='mdi-update'
  226. :label='$t(`admin:auth.tokenRenewalPeriod`)'
  227. :hint='$t(`admin:auth.tokenRenewalPeriodHint`)'
  228. persistent-hint
  229. )
  230. component(:is='activeModal')
  231. </template>
  232. <script>
  233. import _ from 'lodash'
  234. import { sync } from 'vuex-pathify'
  235. import gql from 'graphql-tag'
  236. import editorStore from '../../store/editor'
  237. /* global WIKI */
  238. WIKI.$store.registerModule('editor', editorStore)
  239. export default {
  240. i18nOptions: { namespaces: 'editor' },
  241. components: {
  242. editorModalMedia: () => import(/* webpackChunkName: "editor", webpackMode: "lazy" */ '../editor/editor-modal-media.vue')
  243. },
  244. data() {
  245. return {
  246. config: {
  247. uploadMaxFileSize: 0,
  248. uploadMaxFiles: 0,
  249. uploadScanSVG: true,
  250. uploadForceDownload: true,
  251. securityOpenRedirect: true,
  252. securityIframe: true,
  253. securityReferrerPolicy: true,
  254. securityTrustProxy: true,
  255. securitySRI: true,
  256. securityHSTS: false,
  257. securityHSTSDuration: 0,
  258. securityCSP: false,
  259. securityCSPDirectives: '',
  260. authAutoLogin: false,
  261. authHideLocal: false,
  262. authLoginBgUrl: '',
  263. authJwtAudience: 'urn:wiki.js',
  264. authJwtExpiration: '30m',
  265. authJwtRenewablePeriod: '14d'
  266. },
  267. hstsDurations: [
  268. { value: 300, text: '5 minutes' },
  269. { value: 86400, text: '1 day' },
  270. { value: 604800, text: '1 week' },
  271. { value: 2592000, text: '1 month' },
  272. { value: 31536000, text: '1 year' },
  273. { value: 63072000, text: '2 years' }
  274. ]
  275. }
  276. },
  277. computed: {
  278. activeModal: sync('editor/activeModal')
  279. },
  280. methods: {
  281. async save () {
  282. try {
  283. await this.$apollo.mutate({
  284. mutation: gql`
  285. mutation (
  286. $authAutoLogin: Boolean
  287. $authEnforce2FA: Boolean
  288. $authHideLocal: Boolean
  289. $authLoginBgUrl: String
  290. $authJwtAudience: String
  291. $authJwtExpiration: String
  292. $authJwtRenewablePeriod: String
  293. $uploadMaxFileSize: Int
  294. $uploadMaxFiles: Int
  295. $uploadScanSVG: Boolean
  296. $uploadForceDownload: Boolean
  297. $securityOpenRedirect: Boolean
  298. $securityIframe: Boolean
  299. $securityReferrerPolicy: Boolean
  300. $securityTrustProxy: Boolean
  301. $securitySRI: Boolean
  302. $securityHSTS: Boolean
  303. $securityHSTSDuration: Int
  304. $securityCSP: Boolean
  305. $securityCSPDirectives: String
  306. ) {
  307. site {
  308. updateConfig(
  309. authAutoLogin: $authAutoLogin,
  310. authEnforce2FA: $authEnforce2FA,
  311. authHideLocal: $authHideLocal,
  312. authLoginBgUrl: $authLoginBgUrl,
  313. authJwtAudience: $authJwtAudience,
  314. authJwtExpiration: $authJwtExpiration,
  315. authJwtRenewablePeriod: $authJwtRenewablePeriod,
  316. uploadMaxFileSize: $uploadMaxFileSize,
  317. uploadMaxFiles: $uploadMaxFiles,
  318. uploadScanSVG: $uploadScanSVG
  319. uploadForceDownload: $uploadForceDownload,
  320. securityOpenRedirect: $securityOpenRedirect,
  321. securityIframe: $securityIframe,
  322. securityReferrerPolicy: $securityReferrerPolicy,
  323. securityTrustProxy: $securityTrustProxy,
  324. securitySRI: $securitySRI,
  325. securityHSTS: $securityHSTS,
  326. securityHSTSDuration: $securityHSTSDuration,
  327. securityCSP: $securityCSP,
  328. securityCSPDirectives: $securityCSPDirectives
  329. ) {
  330. responseResult {
  331. succeeded
  332. errorCode
  333. slug
  334. message
  335. }
  336. }
  337. }
  338. }
  339. `,
  340. variables: {
  341. authAutoLogin: _.get(this.config, 'authAutoLogin', false),
  342. authEnforce2FA: _.get(this.config, 'authEnforce2FA', false),
  343. authHideLocal: _.get(this.config, 'authHideLocal', false),
  344. authLoginBgUrl: _.get(this.config, 'authLoginBgUrl', ''),
  345. authJwtAudience: _.get(this.config, 'authJwtAudience', ''),
  346. authJwtExpiration: _.get(this.config, 'authJwtExpiration', ''),
  347. authJwtRenewablePeriod: _.get(this.config, 'authJwtRenewablePeriod', ''),
  348. uploadMaxFileSize: _.toSafeInteger(_.get(this.config, 'uploadMaxFileSize', 0)),
  349. uploadMaxFiles: _.toSafeInteger(_.get(this.config, 'uploadMaxFiles', 0)),
  350. uploadScanSVG: _.get(this.config, 'uploadScanSVG', false),
  351. uploadForceDownload: _.get(this.config, 'uploadForceDownload', false),
  352. securityOpenRedirect: _.get(this.config, 'securityOpenRedirect', false),
  353. securityIframe: _.get(this.config, 'securityIframe', false),
  354. securityReferrerPolicy: _.get(this.config, 'securityReferrerPolicy', false),
  355. securityTrustProxy: _.get(this.config, 'securityTrustProxy', false),
  356. securitySRI: _.get(this.config, 'securitySRI', false),
  357. securityHSTS: _.get(this.config, 'securityHSTS', false),
  358. securityHSTSDuration: _.get(this.config, 'securityHSTSDuration', 0),
  359. securityCSP: _.get(this.config, 'securityCSP', false),
  360. securityCSPDirectives: _.get(this.config, 'securityCSPDirectives', '')
  361. },
  362. watchLoading (isLoading) {
  363. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')
  364. }
  365. })
  366. this.$store.commit('showNotification', {
  367. style: 'success',
  368. message: 'Configuration saved successfully.',
  369. icon: 'check'
  370. })
  371. } catch (err) {
  372. this.$store.commit('pushGraphError', err)
  373. }
  374. },
  375. browseLoginBg () {
  376. this.$store.set('editor/editorKey', 'common')
  377. this.activeModal = 'editorModalMedia'
  378. }
  379. },
  380. mounted () {
  381. this.$root.$on('editorInsert', opts => {
  382. this.config.authLoginBgUrl = opts.path
  383. })
  384. },
  385. beforeDestroy() {
  386. this.$root.$off('editorInsert')
  387. },
  388. apollo: {
  389. config: {
  390. query: gql`
  391. {
  392. site {
  393. config {
  394. authAutoLogin
  395. authEnforce2FA
  396. authHideLocal
  397. authLoginBgUrl
  398. authJwtAudience
  399. authJwtExpiration
  400. authJwtRenewablePeriod
  401. uploadMaxFileSize
  402. uploadMaxFiles
  403. uploadScanSVG
  404. uploadForceDownload
  405. securityOpenRedirect
  406. securityIframe
  407. securityReferrerPolicy
  408. securityTrustProxy
  409. securitySRI
  410. securityHSTS
  411. securityHSTSDuration
  412. securityCSP
  413. securityCSPDirectives
  414. }
  415. }
  416. }
  417. `,
  418. fetchPolicy: 'network-only',
  419. update: (data) => _.cloneDeep(data.site.config),
  420. watchLoading (isLoading) {
  421. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-security-refresh')
  422. }
  423. }
  424. }
  425. }
  426. </script>
  427. <style lang='scss'>
  428. </style>