mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 06:03:42 -05:00
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
This commit is contained in:
parent
116f167a6f
commit
a5637651b5
28 changed files with 115 additions and 51 deletions
53
services/web/app/src/infrastructure/CSP.js
Normal file
53
services/web/app/src/infrastructure/CSP.js
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ const logger = require('logger-sharelatex')
|
|||
const metrics = require('@overleaf/metrics')
|
||||
const expressLocals = require('./ExpressLocals')
|
||||
const Validation = require('./Validation')
|
||||
const csp = require('./CSP')
|
||||
const Router = require('../router')
|
||||
const helmet = require('helmet')
|
||||
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)
|
||||
const server = require('http').createServer(app)
|
||||
|
||||
|
|
|
@ -4,12 +4,13 @@ html(
|
|||
lang=(currentLngCode || 'en')
|
||||
)
|
||||
- metadata = metadata || {}
|
||||
|
||||
block vars
|
||||
|
||||
head
|
||||
include ./_metadata.pug
|
||||
|
||||
script(type="text/javascript").
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
// Stop superfish from loading
|
||||
window.similarproducts = true
|
||||
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
|
||||
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(){
|
||||
(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)
|
||||
})(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('send', 'pageview');
|
||||
|
||||
|
@ -54,7 +55,7 @@ html(
|
|||
if gaOptimize === true && typeof(gaOptimizeId) != "undefined"
|
||||
//- Anti-flicker snippet
|
||||
style(type='text/css') .async-hide { opacity: 0 !important}
|
||||
script(type='text/javascript').
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
if (!ga.isBlocked) {
|
||||
ga('require', '#{gaOptimizeId}');
|
||||
ga('send', 'event', 'pageview', document.title.substring(0, 499), window.location.href.substring(0, 499));
|
||||
|
@ -66,7 +67,7 @@ html(
|
|||
}
|
||||
|
||||
else
|
||||
script(type='text/javascript').
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.ga = function() { console.log("would send to GA", arguments) };
|
||||
|
||||
block meta
|
||||
|
@ -106,9 +107,9 @@ html(
|
|||
block head-scripts
|
||||
|
||||
|
||||
body
|
||||
body(ng-csp=(cspEnabled ? "no-unsafe-eval" : false))
|
||||
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")
|
||||
|
@ -122,9 +123,9 @@ html(
|
|||
!= moduleIncludes("contactModal", locals)
|
||||
|
||||
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
|
||||
var cdnBlocked = typeof Frontend === 'undefined'
|
||||
//- Prevent loops
|
||||
|
|
|
@ -159,12 +159,10 @@ block content
|
|||
h3 {{ title }}
|
||||
.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").
|
||||
!{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.project_id = "!{project_id}";
|
||||
window.userSettings = window.data.userSettings;
|
||||
|
@ -192,11 +190,11 @@ block content
|
|||
window.showReactAddFilesModal = "!{showReactAddFilesModal}" === 'true'
|
||||
|
||||
if (settings.overleaf != null)
|
||||
script(type='text/javascript').
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.overallThemes = JSON.parse('!{StringHelper.stringifyJsonForScript(overallThemes)}');
|
||||
|
||||
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'))
|
||||
|
|
|
@ -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}, {})
|
||||
script(type="text/javascript").
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.fileActionI18n = JSON.parse('!{StringHelper.stringifyJsonForScript(fileActionI18n)}')
|
||||
|
|
|
@ -25,7 +25,7 @@ block content
|
|||
input(type="hidden" name="brandVariationId" value=brandVariationId)
|
||||
|
||||
block append foot-scripts
|
||||
script.
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
$(document).ready(function(){
|
||||
$('#create_form').submit();
|
||||
});
|
||||
|
|
|
@ -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("clearing")}…
|
||||
|
||||
script(type="text/javascript").
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.showNewLogsUI = #{showNewLogsUI || false}
|
||||
window.logsUISubvariant = !{logsUISubvariant ? '"' + logsUISubvariant + '"' : 'null'}
|
||||
|
|
|
@ -62,3 +62,6 @@ block content
|
|||
include ./list/modals
|
||||
|
||||
//- include ./list/front-chat
|
||||
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.userHasNoSubscription = #{!!(settings.enableSubscriptions && !hasSubscription)}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
if (frontChatWidgetRoomId)
|
||||
script.
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
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")
|
||||
|
|
|
@ -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
|
||||
a(href="https://www.overleaf.com/events/wfh2021").btn.btn-primary Upgrade
|
||||
|
||||
|
||||
|
||||
script.
|
||||
window.userHasNoSubscription = #{!!(settings.enableSubscriptions && !hasSubscription)}
|
||||
|
|
|
@ -92,7 +92,7 @@ block content
|
|||
|
||||
|
||||
block append foot-scripts
|
||||
script.
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
$(document).ready(function () {
|
||||
setTimeout(function() {
|
||||
$('.loading-screen-brand').css('height', '20%')
|
||||
|
|
|
@ -122,7 +122,7 @@ block content
|
|||
|
||||
|
||||
block append foot-scripts
|
||||
script(type='text/javascript').
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
$(document).ready(function () {
|
||||
$.ajax({dataType: "script", cache: true, url: "//connect.facebook.net/en_US/all.js"}).done(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() {
|
||||
$(".twitter").click(function() {
|
||||
ga('send', 'event', 'referal-button', 'clicked', "twitter")
|
||||
|
|
|
@ -3,7 +3,7 @@ extends ../layout
|
|||
include ./dashboard/_team_name_mixin
|
||||
|
||||
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
|
||||
main.content.content-alt(ng-cloak)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
script(type='text/javascript').
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.managedInstitutions = !{StringHelper.stringifyJsonForScript(managedInstitutions)}
|
||||
|
||||
each institution in managedInstitutions
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
script(type='text/javascript').
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.recurlyApiKey = "!{settings.apis.recurly.publicKey}"
|
||||
window.subscription = !{StringHelper.stringifyJsonForScript(personalSubscription)}
|
||||
window.recomendedCurrency = "#{personalSubscription.recurly.currency}"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
extends ../layout
|
||||
|
||||
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.recurlyApiKey = "!{settings.apis.recurly.publicKey}"
|
||||
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.
|
||||
|
||||
|
||||
script(type="text/javascript").
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}")
|
||||
|
||||
script(
|
||||
|
|
|
@ -7,7 +7,7 @@ block vars
|
|||
- metadata = { viewport: true }
|
||||
|
||||
block head-scripts
|
||||
script(type='text/javascript').
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.recomendedCurrency = '#{recomendedCurrency}';
|
||||
window.abCurrencyFlag = '#{abCurrencyFlag}';
|
||||
window.groupPlans = !{StringHelper.stringifyJsonForScript(groupPlans)};
|
||||
|
|
|
@ -29,7 +29,7 @@ block content
|
|||
p
|
||||
a.btn.btn-primary(href="/project") < #{translate("back_to_your_projects")}
|
||||
|
||||
script(type="text/javascript").
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.ab = [
|
||||
{step:1, bucket:"student_control", testName:"editor_plan"},
|
||||
{step:1, bucket:"collab_test", testName:"editor_plan"},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
extends ../../layout
|
||||
|
||||
block head-scripts
|
||||
script(type='text/javascript').
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.teamId = '#{teamId}'
|
||||
window.hasIndividualRecurlySubscription = #{hasIndividualRecurlySubscription}
|
||||
window.inviteToken = '#{inviteToken}'
|
||||
|
|
|
@ -2,7 +2,7 @@ extends ../layout
|
|||
|
||||
|
||||
block head-scripts
|
||||
script(type='text/javascript').
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.otherSessions = !{StringHelper.stringifyJsonForScript(sessions)}
|
||||
|
||||
|
||||
|
|
|
@ -56,6 +56,6 @@ block content
|
|||
) #{translate("set_new_password")}
|
||||
|
||||
|
||||
script(type='text/javascript').
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.usersEmail = "#{getReqQueryParam('email')}"
|
||||
window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}
|
||||
|
|
|
@ -284,9 +284,9 @@ block content
|
|||
script#data(type="application/json").
|
||||
!{StringHelper.stringifyJsonForScript({ reconfirmationRemoveEmail, reconfirmedViaSAML })}
|
||||
|
||||
script(type="text/javascript").
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
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.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
block head-scripts
|
||||
script(type='text/javascript').
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.oauthProviders = !{StringHelper.stringifyJsonForScript(oauthProviders)}
|
||||
window.thirdPartyIds = !{StringHelper.stringifyJsonForScript(thirdPartyIds)}
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ block content
|
|||
a(href=paths.exportMembers) #{translate('export_csv')}
|
||||
|
||||
|
||||
script(type="text/javascript").
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.users = !{StringHelper.stringifyJsonForScript(users)};
|
||||
window.paths = !{StringHelper.stringifyJsonForScript(paths)};
|
||||
window.groupSize = #{groupSize || 'null'};
|
||||
|
|
|
@ -689,3 +689,10 @@ module.exports = settings =
|
|||
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,14 +2,14 @@ extends ../../../../app/views/layout
|
|||
|
||||
block content
|
||||
|
||||
script(type="text/javascript").
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.data = {
|
||||
adminUserExists: !{adminUserExists == true},
|
||||
ideJsPath: "!{buildJsPath('ide.js')}",
|
||||
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.
|
||||
hr { margin-bottom: 5px; }
|
||||
|
|
|
@ -59,5 +59,5 @@ block content
|
|||
span(ng-show="!activationForm.inflight") #{translate("activate")}
|
||||
span(ng-show="activationForm.inflight") #{translate("activating")}…
|
||||
|
||||
script(type='text/javascript').
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}
|
||||
|
|
|
@ -7,7 +7,7 @@ module.exports = merge(base, {
|
|||
mode: 'development',
|
||||
|
||||
// Enable accurate source maps for dev
|
||||
devtool: 'eval-source-map',
|
||||
devtool: 'source-map',
|
||||
|
||||
plugins: [
|
||||
// Extract CSS to a separate file (rather than inlining to a <style> tag)
|
||||
|
|
Loading…
Reference in a new issue