Просмотр исходного кода

Add Content-Security-Policy header (#3783)

* Add Content-Security-Policy header
* Add nonce attribute to script tags
* Use source-map for webpack devtool
* Add ng-csp attribute when CSP is enabled
* Allow overriding CSP settings with environment variables
* Hook into render and allow routes to disable the CSP header

GitOrigin-RevId: a873736a3514198165f1b2f1e18d002b65f20d30
Alf Eaton 5 лет назад
Родитель
Сommit
a5637651b5
28 измененных файлов с 115 добавлено и 51 удалено
  1. 53 0
      services/web/app/src/infrastructure/CSP.js
  2. 7 0
      services/web/app/src/infrastructure/Server.js
  3. 11 10
      services/web/app/views/layout.pug
  4. 6 8
      services/web/app/views/project/editor.pug
  5. 1 1
      services/web/app/views/project/editor/history/fileTreeV2.pug
  6. 1 1
      services/web/app/views/project/editor/new_from_template.pug
  7. 1 1
      services/web/app/views/project/editor/pdf.pug
  8. 3 0
      services/web/app/views/project/list.pug
  9. 2 2
      services/web/app/views/project/list/front-chat.pug
  10. 0 5
      services/web/app/views/project/list/side-bar.pug
  11. 1 1
      services/web/app/views/project/token/access.pug
  12. 3 3
      services/web/app/views/referal/bonus.pug
  13. 1 1
      services/web/app/views/subscriptions/dashboard.pug
  14. 1 1
      services/web/app/views/subscriptions/dashboard/_managed_institutions.pug
  15. 1 1
      services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug
  16. 3 3
      services/web/app/views/subscriptions/new.pug
  17. 1 1
      services/web/app/views/subscriptions/plans.pug
  18. 1 1
      services/web/app/views/subscriptions/successful_subscription.pug
  19. 1 1
      services/web/app/views/subscriptions/team/invite.pug
  20. 1 1
      services/web/app/views/user/sessions.pug
  21. 1 1
      services/web/app/views/user/setPassword.pug
  22. 2 2
      services/web/app/views/user/settings.pug
  23. 1 1
      services/web/app/views/user/settings/user-oauth.pug
  24. 1 1
      services/web/app/views/user_membership/index.pug
  25. 7 0
      services/web/config/settings.defaults.coffee
  26. 2 2
      services/web/modules/launchpad/app/views/launchpad.pug
  27. 1 1
      services/web/modules/user-activate/app/views/user/activate.pug
  28. 1 1
      services/web/webpack.config.dev.js

+ 53 - 0
services/web/app/src/infrastructure/CSP.js

@@ -0,0 +1,53 @@
+const crypto = require('crypto')
+
+module.exports = function({
+  reportUri,
+  reportOnly = false,
+  exclude = [],
+  percentage
+}) {
+  return function(req, res, next) {
+    const originalRender = res.render
+
+    res.render = (...args) => {
+      // use the view path after removing any prefix up to a "views" folder
+      const view = args[0].split('/views/').pop()
+
+      // enable the CSP header for a percentage of requests
+      const belowCutoff = Math.random() * 100 <= percentage
+
+      if (belowCutoff && !exclude.includes(view)) {
+        res.locals.cspEnabled = true
+
+        const scriptNonce = crypto.randomBytes(16).toString('base64')
+
+        res.locals.scriptNonce = scriptNonce
+
+        const directives = [
+          `script-src 'nonce-${scriptNonce}' 'unsafe-inline' 'strict-dynamic' https:`,
+          `object-src 'none'`,
+          `base-uri 'none'`
+        ]
+
+        if (reportUri) {
+          directives.push(`report-uri ${reportUri}`)
+          // NOTE: implement report-to once it's more widely supported
+        }
+
+        const policy = directives.join('; ')
+
+        // Note: https://csp-evaluator.withgoogle.com/ is useful for checking the policy
+
+        const header = reportOnly
+          ? 'Content-Security-Policy-Report-Only'
+          : 'Content-Security-Policy'
+
+        res.set(header, policy)
+      }
+
+      originalRender.apply(res, args)
+    }
+
+    next()
+  }
+}

+ 7 - 0
services/web/app/src/infrastructure/Server.js

@@ -5,6 +5,7 @@ const logger = require('logger-sharelatex')
 const metrics = require('@overleaf/metrics')
 const metrics = require('@overleaf/metrics')
 const expressLocals = require('./ExpressLocals')
 const expressLocals = require('./ExpressLocals')
 const Validation = require('./Validation')
 const Validation = require('./Validation')
+const csp = require('./CSP')
 const Router = require('../router')
 const Router = require('../router')
 const helmet = require('helmet')
 const helmet = require('helmet')
 const UserSessionsRedis = require('../Features/User/UserSessionsRedis')
 const UserSessionsRedis = require('../Features/User/UserSessionsRedis')
@@ -221,6 +222,12 @@ webRouter.use(
   })
   })
 )
 )
 
 
+// add CSP header to HTML-rendering routes, if enabled
+if (Settings.csp && Settings.csp.enabled) {
+  logger.info('adding CSP header to rendered routes', Settings.csp)
+  webRouter.use(csp(Settings.csp))
+}
+
 logger.info('creating HTTP server'.yellow)
 logger.info('creating HTTP server'.yellow)
 const server = require('http').createServer(app)
 const server = require('http').createServer(app)
 
 

+ 11 - 10
services/web/app/views/layout.pug

@@ -4,12 +4,13 @@ html(
 	lang=(currentLngCode || 'en')
 	lang=(currentLngCode || 'en')
 )
 )
 	- metadata = metadata || {}
 	- metadata = metadata || {}
+
 	block vars
 	block vars
 
 
 	head
 	head
 		include ./_metadata.pug
 		include ./_metadata.pug
 
 
-		script(type="text/javascript").
+		script(type="text/javascript", nonce=scriptNonce).
 			// Stop superfish from loading
 			// Stop superfish from loading
 			window.similarproducts = true
 			window.similarproducts = true
 		style [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak  {display: none !important; display: none; }
 		style [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak  {display: none !important; display: none; }
@@ -32,12 +33,12 @@ html(
 
 
 		//- Google Analytics
 		//- Google Analytics
 		if (typeof(gaToken) != "undefined")
 		if (typeof(gaToken) != "undefined")
-			script(type='text/javascript').
+			script(type="text/javascript", nonce=scriptNonce).
 				(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
 				(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
 				(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
 				(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
 				m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
 				m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
 				})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
 				})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
-			script(type='text/javascript').
+			script(type="text/javascript", nonce=scriptNonce).
 				ga('create', '#{gaToken}', '#{settings.cookieDomain.replace(/^\./, "")}');
 				ga('create', '#{gaToken}', '#{settings.cookieDomain.replace(/^\./, "")}');
 				ga('send', 'pageview');
 				ga('send', 'pageview');
 
 
@@ -54,7 +55,7 @@ html(
 			if gaOptimize === true && typeof(gaOptimizeId) != "undefined"
 			if gaOptimize === true && typeof(gaOptimizeId) != "undefined"
 				//- Anti-flicker snippet
 				//- Anti-flicker snippet
 				style(type='text/css') .async-hide { opacity: 0 !important}
 				style(type='text/css') .async-hide { opacity: 0 !important}
-				script(type='text/javascript').
+				script(type="text/javascript", nonce=scriptNonce).
 					if (!ga.isBlocked) {
 					if (!ga.isBlocked) {
 						ga('require', '#{gaOptimizeId}');
 						ga('require', '#{gaOptimizeId}');
 						ga('send', 'event', 'pageview',  document.title.substring(0, 499), window.location.href.substring(0, 499));
 						ga('send', 'event', 'pageview',  document.title.substring(0, 499), window.location.href.substring(0, 499));
@@ -66,7 +67,7 @@ html(
 					}
 					}
 
 
 		else
 		else
-			script(type='text/javascript').
+			script(type="text/javascript", nonce=scriptNonce).
 				window.ga = function() { console.log("would send to GA", arguments) };
 				window.ga = function() { console.log("would send to GA", arguments) };
 
 
 		block meta
 		block meta
@@ -106,9 +107,9 @@ html(
 		block head-scripts
 		block head-scripts
 
 
 
 
-	body
+	body(ng-csp=(cspEnabled ? "no-unsafe-eval" : false))
 		if(settings.recaptcha && settings.recaptcha.siteKeyV3)
 		if(settings.recaptcha && settings.recaptcha.siteKeyV3)
-			script(src="https://www.google.com/recaptcha/api.js?render="+settings.recaptcha.siteKeyV3)
+			script(type="text/javascript", nonce=scriptNonce, src="https://www.google.com/recaptcha/api.js?render="+settings.recaptcha.siteKeyV3)
 
 
 
 
 		if (typeof(suppressNavbar) == "undefined")
 		if (typeof(suppressNavbar) == "undefined")
@@ -122,9 +123,9 @@ html(
 		!= moduleIncludes("contactModal", locals)
 		!= moduleIncludes("contactModal", locals)
 
 
 		block foot-scripts
 		block foot-scripts
-			script(src=buildJsPath('libraries.js'))
-			script(src=buildJsPath('main.js'))
-			script(type="text/javascript").
+			script(type="text/javascript", nonce=scriptNonce, src=buildJsPath('libraries.js'))
+			script(type="text/javascript", nonce=scriptNonce, src=buildJsPath('main.js'))
+			script(type="text/javascript", nonce=scriptNonce).
 				//- Look for bundle
 				//- Look for bundle
 				var cdnBlocked = typeof Frontend === 'undefined'
 				var cdnBlocked = typeof Frontend === 'undefined'
 				//- Prevent loops
 				//- Prevent loops

+ 6 - 8
services/web/app/views/project/editor.pug

@@ -159,12 +159,10 @@ block content
 			h3 {{ title }}
 			h3 {{ title }}
 		.modal-body(ng-bind-html="message")
 		.modal-body(ng-bind-html="message")
 
 
-	//- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/script>'
-	//- and doesn't prematurely end the script tag.
 	script#data(type="application/json").
 	script#data(type="application/json").
 		!{StringHelper.stringifyJsonForScript({ userSettings: userSettings, user: user, trackChangesState: trackChangesState, useV2History: useV2History, enabledLinkedFileTypes: settings.enabledLinkedFileTypes, brandVariation: brandVariation })}
 		!{StringHelper.stringifyJsonForScript({ userSettings: userSettings, user: user, trackChangesState: trackChangesState, useV2History: useV2History, enabledLinkedFileTypes: settings.enabledLinkedFileTypes, brandVariation: brandVariation })}
 
 
-	script(type='text/javascript').
+	script(type="text/javascript", nonce=scriptNonce).
 		window.data = JSON.parse(document.querySelector("#data").text);
 		window.data = JSON.parse(document.querySelector("#data").text);
 		window.project_id = "!{project_id}";
 		window.project_id = "!{project_id}";
 		window.userSettings = window.data.userSettings;
 		window.userSettings = window.data.userSettings;
@@ -192,11 +190,11 @@ block content
 		window.showReactAddFilesModal = "!{showReactAddFilesModal}" === 'true'
 		window.showReactAddFilesModal = "!{showReactAddFilesModal}" === 'true'
 
 
 	if (settings.overleaf != null)
 	if (settings.overleaf != null)
-		script(type='text/javascript').
+		script(type="text/javascript", nonce=scriptNonce).
 			window.overallThemes = JSON.parse('!{StringHelper.stringifyJsonForScript(overallThemes)}');
 			window.overallThemes = JSON.parse('!{StringHelper.stringifyJsonForScript(overallThemes)}');
 
 
 block foot-scripts
 block foot-scripts
-	script(type="text/javascript" src=(wsUrl || '/socket.io') + '/socket.io.js')
-	script(src=mathJaxPath)
-	script(src=buildJsPath('libraries.js'))
-	script(src=buildJsPath('ide.js'))
+	script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js')
+	script(type="text/javascript", nonce=scriptNonce, src=mathJaxPath)
+	script(type="text/javascript", nonce=scriptNonce, src=buildJsPath('libraries.js'))
+	script(type="text/javascript", nonce=scriptNonce, src=buildJsPath('ide.js'))

+ 1 - 1
services/web/app/views/project/editor/history/fileTreeV2.pug

@@ -57,5 +57,5 @@ script(type="text/ng-template", id="historyFileEntityTpl")
 			)
 			)
 
 
 - var fileActionI18n = ['edited', 'renamed', 'created', 'deleted'].reduce((acc, i) => {acc[i] = translate('file_action_' + i); return acc}, {})
 - var fileActionI18n = ['edited', 'renamed', 'created', 'deleted'].reduce((acc, i) => {acc[i] = translate('file_action_' + i); return acc}, {})
-script(type="text/javascript").
+script(type="text/javascript", nonce=scriptNonce).
 	window.fileActionI18n = JSON.parse('!{StringHelper.stringifyJsonForScript(fileActionI18n)}')
 	window.fileActionI18n = JSON.parse('!{StringHelper.stringifyJsonForScript(fileActionI18n)}')

+ 1 - 1
services/web/app/views/project/editor/new_from_template.pug

@@ -25,7 +25,7 @@ block content
       input(type="hidden" name="brandVariationId" value=brandVariationId)
       input(type="hidden" name="brandVariationId" value=brandVariationId)
 
 
 block append foot-scripts
 block append foot-scripts
-  script.
+  script(type="text/javascript", nonce=scriptNonce).
     $(document).ready(function(){
     $(document).ready(function(){
       $('#create_form').submit();
       $('#create_form').submit();
     });
     });

+ 1 - 1
services/web/app/views/project/editor/pdf.pug

@@ -414,6 +414,6 @@ script(type='text/ng-template', id='clearCacheModalTemplate')
 			span(ng-show="!state.inflight") #{translate("clear_cache")}
 			span(ng-show="!state.inflight") #{translate("clear_cache")}
 			span(ng-show="state.inflight") #{translate("clearing")}…
 			span(ng-show="state.inflight") #{translate("clearing")}…
 
 
-script(type="text/javascript").
+script(type="text/javascript", nonce=scriptNonce).
 	window.showNewLogsUI = #{showNewLogsUI || false}
 	window.showNewLogsUI = #{showNewLogsUI || false}
 	window.logsUISubvariant = !{logsUISubvariant ? '"' + logsUISubvariant + '"' : 'null'}
 	window.logsUISubvariant = !{logsUISubvariant ? '"' + logsUISubvariant + '"' : 'null'}

+ 3 - 0
services/web/app/views/project/list.pug

@@ -62,3 +62,6 @@ block content
 		include ./list/modals
 		include ./list/modals
 
 
 	//- include ./list/front-chat
 	//- include ./list/front-chat
+
+	script(type="text/javascript", nonce=scriptNonce).
+		window.userHasNoSubscription = #{!!(settings.enableSubscriptions && !hasSubscription)}

+ 2 - 2
services/web/app/views/project/list/front-chat.pug

@@ -1,4 +1,4 @@
 if (frontChatWidgetRoomId)
 if (frontChatWidgetRoomId)
-	script.
+	script(type="text/javascript", nonce=scriptNonce).
 		window.FCSP = '#{frontChatWidgetRoomId}';
 		window.FCSP = '#{frontChatWidgetRoomId}';
-	script(src="https://chat-assets.frontapp.com/v1/chat.bundle.js")
+	script(type="text/javascript", nonce=scriptNonce, src="https://chat-assets.frontapp.com/v1/chat.bundle.js")

+ 0 - 5
services/web/app/views/project/list/side-bar.pug

@@ -161,8 +161,3 @@ if (showUserDetailsArea)
 			p.small To help you work from home throughout 2021, we're providing discounted plans and special initiatives.
 			p.small To help you work from home throughout 2021, we're providing discounted plans and special initiatives.
 			p
 			p
 				a(href="https://www.overleaf.com/events/wfh2021").btn.btn-primary Upgrade
 				a(href="https://www.overleaf.com/events/wfh2021").btn.btn-primary Upgrade
-
-
-			
-script.
-	window.userHasNoSubscription = #{!!(settings.enableSubscriptions && !hasSubscription)}

+ 1 - 1
services/web/app/views/project/token/access.pug

@@ -92,7 +92,7 @@ block content
 
 
 
 
 block append foot-scripts
 block append foot-scripts
-	script.
+	script(type="text/javascript", nonce=scriptNonce).
 		$(document).ready(function () {
 		$(document).ready(function () {
 			setTimeout(function() {
 			setTimeout(function() {
 				$('.loading-screen-brand').css('height', '20%')
 				$('.loading-screen-brand').css('height', '20%')

+ 3 - 3
services/web/app/views/referal/bonus.pug

@@ -122,7 +122,7 @@ block content
 
 
 
 
 block append foot-scripts
 block append foot-scripts
-	script(type='text/javascript').
+	script(type="text/javascript", nonce=scriptNonce).
 		$(document).ready(function () {
 		$(document).ready(function () {
 			$.ajax({dataType: "script", cache: true, url: "//connect.facebook.net/en_US/all.js"}).done(function () {
 			$.ajax({dataType: "script", cache: true, url: "//connect.facebook.net/en_US/all.js"}).done(function () {
 				window.fbAsyncInit = function() {
 				window.fbAsyncInit = function() {
@@ -148,9 +148,9 @@ block append foot-scripts
 			}
 			}
 		}
 		}
 
 
-	script(type='text/javascript', src='//platform.twitter.com/widgets.js')
+	script(type="text/javascript", nonce=scriptNonce, src='//platform.twitter.com/widgets.js')
 
 
-	script(type="text/javascript").
+	script(type="text/javascript", nonce=scriptNonce).
 		$(function() {
 		$(function() {
 			$(".twitter").click(function() {
 			$(".twitter").click(function() {
 				ga('send', 'event', 'referal-button', 'clicked', "twitter")
 				ga('send', 'event', 'referal-button', 'clicked', "twitter")

+ 1 - 1
services/web/app/views/subscriptions/dashboard.pug

@@ -3,7 +3,7 @@ extends ../layout
 include ./dashboard/_team_name_mixin
 include ./dashboard/_team_name_mixin
 
 
 block head-scripts
 block head-scripts
-	script(src="https://js.recurly.com/v4/recurly.js")
+	script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js")
 
 
 block content
 block content
 	main.content.content-alt(ng-cloak)
 	main.content.content-alt(ng-cloak)

+ 1 - 1
services/web/app/views/subscriptions/dashboard/_managed_institutions.pug

@@ -1,4 +1,4 @@
-script(type='text/javascript').
+script(type="text/javascript", nonce=scriptNonce).
 	window.managedInstitutions = !{StringHelper.stringifyJsonForScript(managedInstitutions)}
 	window.managedInstitutions = !{StringHelper.stringifyJsonForScript(managedInstitutions)}
 
 
 each institution in managedInstitutions
 each institution in managedInstitutions

+ 1 - 1
services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug

@@ -1,4 +1,4 @@
-script(type='text/javascript').
+script(type="text/javascript", nonce=scriptNonce).
 	window.recurlyApiKey = "!{settings.apis.recurly.publicKey}"
 	window.recurlyApiKey = "!{settings.apis.recurly.publicKey}"
 	window.subscription = !{StringHelper.stringifyJsonForScript(personalSubscription)}
 	window.subscription = !{StringHelper.stringifyJsonForScript(personalSubscription)}
 	window.recomendedCurrency = "#{personalSubscription.recurly.currency}"
 	window.recomendedCurrency = "#{personalSubscription.recurly.currency}"

+ 3 - 3
services/web/app/views/subscriptions/new.pug

@@ -1,8 +1,8 @@
 extends ../layout
 extends ../layout
 
 
 block head-scripts
 block head-scripts
-	script(src="https://js.recurly.com/v4/recurly.js")
-	script(type='text/javascript').
+	script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js")
+	script(type="text/javascript", nonce=scriptNonce).
 		window.countryCode = !{StringHelper.stringifyJsonForScript(countryCode || '')}
 		window.countryCode = !{StringHelper.stringifyJsonForScript(countryCode || '')}
 		window.recurlyApiKey = "!{settings.apis.recurly.publicKey}"
 		window.recurlyApiKey = "!{settings.apis.recurly.publicKey}"
 		window.recomendedCurrency = !{StringHelper.stringifyJsonForScript(String(currency).slice(0,3))}
 		window.recomendedCurrency = !{StringHelper.stringifyJsonForScript(String(currency).slice(0,3))}
@@ -352,7 +352,7 @@ block content
 							p.small.text-center(ng-non-bindable) We're confident that you'll love #{settings.appName}, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days.
 							p.small.text-center(ng-non-bindable) We're confident that you'll love #{settings.appName}, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days.
 
 
 
 
-	script(type="text/javascript").
+	script(type="text/javascript", nonce=scriptNonce).
 		ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}")
 		ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}")
 
 
 	script(
 	script(

+ 1 - 1
services/web/app/views/subscriptions/plans.pug

@@ -7,7 +7,7 @@ block vars
 	- metadata = { viewport: true }
 	- metadata = { viewport: true }
 
 
 block head-scripts
 block head-scripts
-	script(type='text/javascript').
+	script(type="text/javascript", nonce=scriptNonce).
 		window.recomendedCurrency = '#{recomendedCurrency}';
 		window.recomendedCurrency = '#{recomendedCurrency}';
 		window.abCurrencyFlag = '#{abCurrencyFlag}';
 		window.abCurrencyFlag = '#{abCurrencyFlag}';
 		window.groupPlans = !{StringHelper.stringifyJsonForScript(groupPlans)};
 		window.groupPlans = !{StringHelper.stringifyJsonForScript(groupPlans)};

+ 1 - 1
services/web/app/views/subscriptions/successful_subscription.pug

@@ -29,7 +29,7 @@ block content
 						p
 						p
 							a.btn.btn-primary(href="/project") &lt; #{translate("back_to_your_projects")}
 							a.btn.btn-primary(href="/project") &lt; #{translate("back_to_your_projects")}
 
 
-	script(type="text/javascript").
+	script(type="text/javascript", nonce=scriptNonce).
 		window.ab = [
 		window.ab = [
 			{step:1, bucket:"student_control", testName:"editor_plan"},
 			{step:1, bucket:"student_control", testName:"editor_plan"},
 			{step:1, bucket:"collab_test", testName:"editor_plan"},
 			{step:1, bucket:"collab_test", testName:"editor_plan"},

+ 1 - 1
services/web/app/views/subscriptions/team/invite.pug

@@ -1,7 +1,7 @@
 extends ../../layout
 extends ../../layout
 
 
 block head-scripts
 block head-scripts
-	script(type='text/javascript').
+	script(type="text/javascript", nonce=scriptNonce).
 		window.teamId = '#{teamId}'
 		window.teamId = '#{teamId}'
 		window.hasIndividualRecurlySubscription = #{hasIndividualRecurlySubscription}
 		window.hasIndividualRecurlySubscription = #{hasIndividualRecurlySubscription}
 		window.inviteToken = '#{inviteToken}'
 		window.inviteToken = '#{inviteToken}'

+ 1 - 1
services/web/app/views/user/sessions.pug

@@ -2,7 +2,7 @@ extends ../layout
 
 
 
 
 block head-scripts
 block head-scripts
-	script(type='text/javascript').
+	script(type="text/javascript", nonce=scriptNonce).
 		window.otherSessions = !{StringHelper.stringifyJsonForScript(sessions)}
 		window.otherSessions = !{StringHelper.stringifyJsonForScript(sessions)}
 
 
 
 

+ 1 - 1
services/web/app/views/user/setPassword.pug

@@ -56,6 +56,6 @@ block content
 								) #{translate("set_new_password")}
 								) #{translate("set_new_password")}
 
 
 
 
-	script(type='text/javascript').
+	script(type="text/javascript", nonce=scriptNonce).
 		window.usersEmail = "#{getReqQueryParam('email')}"
 		window.usersEmail = "#{getReqQueryParam('email')}"
 		window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}
 		window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}

+ 2 - 2
services/web/app/views/user/settings.pug

@@ -284,9 +284,9 @@ block content
 	script#data(type="application/json").
 	script#data(type="application/json").
 		!{StringHelper.stringifyJsonForScript({ reconfirmationRemoveEmail, reconfirmedViaSAML })}
 		!{StringHelper.stringifyJsonForScript({ reconfirmationRemoveEmail, reconfirmedViaSAML })}
 
 
-	script(type="text/javascript").
+	script(type="text/javascript", nonce=scriptNonce).
 		window.data = JSON.parse(document.querySelector("#data").text);
 		window.data = JSON.parse(document.querySelector("#data").text);
 
 
-	script(type='text/javascript').
+	script(type="text/javascript", nonce=scriptNonce).
 		window.usersEmail = !{StringHelper.stringifyJsonForScript(user.email)};
 		window.usersEmail = !{StringHelper.stringifyJsonForScript(user.email)};
 		window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}
 		window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}

+ 1 - 1
services/web/app/views/user/settings/user-oauth.pug

@@ -1,5 +1,5 @@
 block head-scripts
 block head-scripts
-	script(type='text/javascript').
+	script(type="text/javascript", nonce=scriptNonce).
 		window.oauthProviders = !{StringHelper.stringifyJsonForScript(oauthProviders)}
 		window.oauthProviders = !{StringHelper.stringifyJsonForScript(oauthProviders)}
 		window.thirdPartyIds = !{StringHelper.stringifyJsonForScript(thirdPartyIds)}
 		window.thirdPartyIds = !{StringHelper.stringifyJsonForScript(thirdPartyIds)}
 
 

+ 1 - 1
services/web/app/views/user_membership/index.pug

@@ -108,7 +108,7 @@ block content
 									a(href=paths.exportMembers) #{translate('export_csv')}
 									a(href=paths.exportMembers) #{translate('export_csv')}
 
 
 
 
-	script(type="text/javascript").
+	script(type="text/javascript", nonce=scriptNonce).
 		window.users = !{StringHelper.stringifyJsonForScript(users)};
 		window.users = !{StringHelper.stringifyJsonForScript(users)};
 		window.paths = !{StringHelper.stringifyJsonForScript(paths)};
 		window.paths = !{StringHelper.stringifyJsonForScript(paths)};
 		window.groupSize = #{groupSize || 'null'};
 		window.groupSize = #{groupSize || 'null'};

+ 7 - 0
services/web/config/settings.defaults.coffee

@@ -689,3 +689,10 @@ module.exports = settings =
 		createFileModes: []
 		createFileModes: []
 	}
 	}
 
 
+	csp: {
+		percentage: parseFloat(process.env.CSP_PERCENTAGE) || 0
+		enabled: process.env.CSP_ENABLED == 'true'
+		reportOnly: process.env.CSP_REPORT_ONLY == 'true'
+		reportUri: process.env.CSP_REPORT_URI
+		exclude: ['project/editor', 'project/list']
+	}

+ 2 - 2
services/web/modules/launchpad/app/views/launchpad.pug

@@ -2,14 +2,14 @@ extends ../../../../app/views/layout
 
 
 block content
 block content
 
 
-	script(type="text/javascript").
+	script(type="text/javascript", nonce=scriptNonce).
 		window.data = {
 		window.data = {
 			adminUserExists: !{adminUserExists == true},
 			adminUserExists: !{adminUserExists == true},
 			ideJsPath: "!{buildJsPath('ide.js')}",
 			ideJsPath: "!{buildJsPath('ide.js')}",
 			authMethod: "!{authMethod}"
 			authMethod: "!{authMethod}"
 		}
 		}
 
 
-	script(type="text/javascript" src=(wsUrl || '/socket.io') + '/socket.io.js')
+	script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js')
 
 
 	style.
 	style.
 		hr { margin-bottom: 5px; }
 		hr { margin-bottom: 5px; }

+ 1 - 1
services/web/modules/user-activate/app/views/user/activate.pug

@@ -59,5 +59,5 @@ block content
 									span(ng-show="!activationForm.inflight") #{translate("activate")}
 									span(ng-show="!activationForm.inflight") #{translate("activate")}
 									span(ng-show="activationForm.inflight") #{translate("activating")}…
 									span(ng-show="activationForm.inflight") #{translate("activating")}…
 
 
-	script(type='text/javascript').
+	script(type="text/javascript", nonce=scriptNonce).
 		window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}
 		window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}

+ 1 - 1
services/web/webpack.config.dev.js

@@ -7,7 +7,7 @@ module.exports = merge(base, {
   mode: 'development',
   mode: 'development',
 
 
   // Enable accurate source maps for dev
   // Enable accurate source maps for dev
-  devtool: 'eval-source-map',
+  devtool: 'source-map',
 
 
   plugins: [
   plugins: [
     // Extract CSS to a separate file (rather than inlining to a <style> tag)
     // Extract CSS to a separate file (rather than inlining to a <style> tag)