diff --git a/server/html.ts b/server/html.ts
index 107ee749a..fabbfd634 100644
--- a/server/html.ts
+++ b/server/html.ts
@@ -1,5 +1,11 @@
+import { createHash } from 'crypto';
import type { Globals } from '../shared/globals';
+interface HtmlAndScriptHashes {
+ body: string;
+ hashes: string[];
+}
+
declare let WEBPACK_BUILD: string;
/**
@@ -11,33 +17,40 @@ declare let WEBPACK_BUILD: string;
*/
const insertGlobals = (globals: Globals) => {
- return ``;
+ return `window.guardian = ${JSON.stringify(globals)}`;
};
-const html: (_: {
+const htmlAndScriptHashes: (_: {
readonly title: string;
readonly src: string;
readonly globals: Globals;
-}) => string = ({ title, src, globals }) => `
-
-
-
-
-
- ${title}
-
- ${insertGlobals(globals)}
-
-
-
-
-
-
-
-
-
-`;
-
-export { html };
+}) => HtmlAndScriptHashes = ({ title, src, globals }) => {
+ const mainScriptBundleSrc = `var s = document.createElement("script"); s.src = "${src}?release=${WEBPACK_BUILD}";document.body.appendChild(s);`;
+ const setGuardianWindowSrc = insertGlobals(globals);
+ return {
+ body: `
+
+
+
+
+
+ ${title}
+
+
+
+
+
+
+
+
+
+
+ `,
+ hashes: [
+ createHash('sha256').update(mainScriptBundleSrc).digest('base64'),
+ createHash('sha256').update(setGuardianWindowSrc).digest('base64'),
+ ],
+ };
+};
+
+export { htmlAndScriptHashes };
diff --git a/server/routes/api.ts b/server/routes/api.ts
index b288d6db6..ff1965edd 100644
--- a/server/routes/api.ts
+++ b/server/routes/api.ts
@@ -1,6 +1,5 @@
import * as Sentry from '@sentry/node';
import { Router } from 'express';
-import { featureSwitches } from '@/shared/featureSwitches';
import type { MembersDataApiResponse } from '@/shared/productResponse';
import { isProduct, MDA_TEST_USER_HEADER } from '@/shared/productResponse';
import {
@@ -354,12 +353,10 @@ router.get(
router.post('/reminders/cancel', cancelReminderHandler);
router.post('/reminders/reactivate', reactivateReminderHandler);
-if (featureSwitches.cspSecurityAudit) {
- router.post('/csp-audit-report-endpoint', (req, res) => {
- const parsedBody = JSON.parse(req.body.toString());
- log.warn(JSON.stringify(parsedBody));
- res.status(204).end();
- });
-}
+router.post('/csp-audit-report-endpoint', (req, res) => {
+ const parsedBody = JSON.parse(req.body.toString());
+ log.warn(JSON.stringify(parsedBody));
+ res.status(204).end();
+});
export { router };
diff --git a/server/routes/helpCentreFrontend.ts b/server/routes/helpCentreFrontend.ts
index 8c00826a6..189fbd480 100644
--- a/server/routes/helpCentreFrontend.ts
+++ b/server/routes/helpCentreFrontend.ts
@@ -2,8 +2,9 @@ import { Router } from 'express';
import type { Request, Response } from 'express';
import { DEFAULT_PAGE_TITLE } from '../../shared/helpCentreConfig';
import { conf } from '../config';
-import { html } from '../html';
+import { htmlAndScriptHashes } from '../html';
import { withIdentity } from '../middleware/identityMiddleware';
+import { createCsp } from '../server';
import {
clientDSN,
getRecaptchaPublicKey,
@@ -12,25 +13,31 @@ import {
const router = Router();
-router.use(withIdentity());
-
-router.use(async (_: Request, res: Response) => {
+router.use(withIdentity(), async (_: Request, res: Response) => {
const title = DEFAULT_PAGE_TITLE;
const src = '/static/help-centre.js';
- res.send(
- html({
- title,
- src,
- globals: {
- domain: conf.DOMAIN,
- dsn: clientDSN,
- identityDetails: res.locals.identity,
- recaptchaPublicKey: await getRecaptchaPublicKey(),
- ...(await getStripePublicKeys()),
- },
- }),
- );
+ const htmlStrAndScriptHashes = htmlAndScriptHashes({
+ title,
+ src,
+ globals: {
+ domain: conf.DOMAIN,
+ dsn: clientDSN,
+ identityDetails: res.locals.identity,
+ recaptchaPublicKey: await getRecaptchaPublicKey(),
+ ...(await getStripePublicKeys()),
+ },
+ });
+
+ res.set({
+ 'Report-To':
+ '{ "group": "csp-endpoint", "endpoints": [ { "url": "/api/csp-audit-report-endpoint" } ] }',
+ 'Content-Security-Policy-Report-Only': createCsp(
+ htmlStrAndScriptHashes.hashes,
+ ),
+ });
+
+ res.send(htmlStrAndScriptHashes);
});
export { router };
diff --git a/server/routes/mmaFrontend.ts b/server/routes/mmaFrontend.ts
index cc50428ab..b004bcefa 100644
--- a/server/routes/mmaFrontend.ts
+++ b/server/routes/mmaFrontend.ts
@@ -1,8 +1,9 @@
import type { Request, Response } from 'express';
import { Router } from 'express';
import { conf } from '../config';
-import { html } from '../html';
+import { htmlAndScriptHashes } from '../html';
import { withIdentity } from '../middleware/identityMiddleware';
+import { createCsp } from '../server';
import { csrfValidateMiddleware } from '../util';
import {
clientDSN,
@@ -23,19 +24,27 @@ router.use(withIdentity(), async (req: Request, res: Response) => {
sameSite: 'strict',
});
- res.send(
- html({
- title,
- src,
- globals: {
- domain: conf.DOMAIN,
- dsn: clientDSN,
- identityDetails: res.locals.identity,
- recaptchaPublicKey: await getRecaptchaPublicKey(),
- ...(await getStripePublicKeys()),
- },
- }),
- );
+ const htmlStrAndScriptHashes = htmlAndScriptHashes({
+ title,
+ src,
+ globals: {
+ domain: conf.DOMAIN,
+ dsn: clientDSN,
+ identityDetails: res.locals.identity,
+ recaptchaPublicKey: await getRecaptchaPublicKey(),
+ ...(await getStripePublicKeys()),
+ },
+ });
+
+ res.set({
+ 'Report-To':
+ '{ "group": "csp-endpoint", "endpoints": [ { "url": "/api/csp-audit-report-endpoint" } ] }',
+ 'Content-Security-Policy-Report-Only': createCsp(
+ htmlStrAndScriptHashes.hashes,
+ ),
+ });
+
+ res.send(htmlStrAndScriptHashes.body);
});
export { router };
diff --git a/server/server.ts b/server/server.ts
index 7698c3e2c..a44dbba53 100644
--- a/server/server.ts
+++ b/server/server.ts
@@ -4,7 +4,6 @@ import cookieParser from 'cookie-parser';
import type { NextFunction, Request, RequestHandler, Response } from 'express';
import { default as express } from 'express';
import helmet from 'helmet';
-import { featureSwitches } from '../shared/featureSwitches';
import { MAX_FILE_ATTACHMENT_SIZE_KB } from '../shared/fileUploadUtils';
import { conf } from './config';
import { log } from './log';
@@ -18,6 +17,7 @@ const server = express();
const oktaConfig = await getConfig();
declare let WEBPACK_BUILD: string;
+
if (conf.SERVER_DSN) {
Sentry.init({
dsn: conf.SERVER_DSN,
@@ -40,29 +40,31 @@ if (conf.DOMAIN === 'thegulocal.com') {
server.use(helmet());
-if (featureSwitches.cspSecurityAudit) {
- const cspDefaultSrcAllowList = [
- "'self'",
- 'https://sourcepoint.theguardian.com',
- 'https://gnm-app.quantummetric.com',
- 'https://assets.guim.co.uk',
- 'https://ophan.theguardian.com',
- ].join(' ');
- const csp = [
- 'report-uri /api/csp-audit-report-endpoint',
- 'report-to csp-endpoint',
- `default-src ${cspDefaultSrcAllowList}`,
- `style-src 'unsafe-inline'`, // this is unsafe but needed for now for emotion
- ];
- server.use(function (_: Request, res: Response, next: NextFunction) {
- res.set({
- 'Report-To':
- '{ "group": "csp-endpoint", "endpoints": [ { "url": "/api/csp-audit-report-endpoint" } ] }',
- 'Content-Security-Policy-Report-Only': `${csp.join('; ')};`,
- });
- next();
+export const createCsp = (hashes: string[]) => {
+ return `
+ script-src ${hashes
+ .map((hash) => `'sha256-${hash}'`)
+ .join(' ')} 'strict-dynamic';
+ style-src 'unsafe-inline';
+ object-src 'none';
+ base-uri 'none';
+ `;
+};
+
+server.use(function (_: Request, res: Response, next: NextFunction) {
+ /*
+ * This sets a default csp header, this is overriden in:
+ * - mmaFrontend.ts
+ * - helpcentreFrontend.ts
+ * Where a more specific policy with script hashes can be added
+ */
+ res.set({
+ 'Report-To':
+ '{ "group": "csp-endpoint", "endpoints": [ { "url": "/api/csp-audit-report-endpoint" } ] }',
+ 'Content-Security-Policy-Report-Only': createCsp([]),
});
-}
+ next();
+});
const serveStaticAssets: RequestHandler = express.static(__dirname + '/static');
diff --git a/shared/featureSwitches.ts b/shared/featureSwitches.ts
index 079b66b9a..ed0e7cf6d 100644
--- a/shared/featureSwitches.ts
+++ b/shared/featureSwitches.ts
@@ -28,7 +28,6 @@ type FeatureSwitchName =
| 'appSubscriptions'
| 'supporterPlusUpdateAmount'
| 'digisubSave'
- | 'cspSecurityAudit'
| 'supporterplusCancellationOffer';
export const featureSwitches: Record = {
@@ -36,6 +35,5 @@ export const featureSwitches: Record = {
appSubscriptions: true,
supporterPlusUpdateAmount: true,
digisubSave: true,
- cspSecurityAudit: true,
supporterplusCancellationOffer: true,
};