Skip to content

Commit

Permalink
Add CSP reporting header (#3583)
Browse files Browse the repository at this point in the history
* inline scripts and put hydration data into json tag

* add csp headers to the static server and lambda

* allow unsafe-inline styles

* add LUX and ga_debug to CSP

* make lint ignore internal require

* remove jsesc and turn into JSON tag

* add wildcarded github & google usercontent URLs

Co-authored-by: Ryan Johnson <escattone@gmail.com>

* add lux

* Make report-uri relative

* log CSP violations in the dev server

* CSP header only for HTML on lambda

* add CSP allow entries from content crawl

* turn CSP into an object and stringify it into different configs

Co-authored-by: Ryan Johnson <escattone@gmail.com>
  • Loading branch information
2 people authored and peterbe committed Jun 1, 2021
1 parent 6df8ccd commit 107cf0e
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 46 deletions.
20 changes: 6 additions & 14 deletions client/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,19 @@ if (!container) {
throw new Error("missing root element");
}

// The SSR rendering will put
// a `<script>window.__data__ = JSON.parse(...)</script>` into the HTML.
// The SSR rendering will put a `<script type="application/json">` into the HTML.
// If it's there, great, if it's not there, this'll be `undefined` the
// components will know to fetch it with XHR.
// TODO: When we have TS types fo `docData` this would become
// something like `(window as any).__data__ as DocData`.
const docData = (window as any).__data__;
const pageNotFound = (window as any).__pageNotFound__;
const feedEntries = (window as any).__feedEntries__;
const possibleLocales = (window as any).__possibleLocales__;
const hydrationElement = document.getElementById("hydration");
const appData = hydrationElement
? JSON.parse(hydrationElement.textContent!)
: {};

let app = (
<GAProvider>
<UserDataProvider>
<Router>
<App
doc={docData}
pageNotFound={pageNotFound}
feedEntries={feedEntries}
possibleLocales={possibleLocales}
/>
<App {...appData} />
</Router>
</UserDataProvider>
</GAProvider>
Expand Down
19 changes: 19 additions & 0 deletions deployer/aws-lambda/content-origin-response/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/* eslint-disable node/no-missing-require */
const { CSP_VALUE_PROD, CSP_VALUE_STAGE } = require("@yari-internal/constants");

exports.handler = async (event) => {
/*
* This Lambda@Edge function is designed to handle origin-response
Expand Down Expand Up @@ -40,5 +43,21 @@ exports.handler = async (event) => {
];
}

const contentType = response.headers["content-type"];
if (
contentType &&
contentType[0] &&
contentType[0].value.startsWith("text/html")
) {
response.headers["content-security-policy-report-only"] = [
{
key: "Content-Security-Policy-Report-Only",
value: request.origin.custom.domainName.startsWith("prod.")
? CSP_VALUE_PROD
: CSP_VALUE_STAGE,
},
];
}

return response;
};
3 changes: 3 additions & 0 deletions deployer/aws-lambda/content-origin-response/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"scripts": {
"make-package": "yarn install && zip -r -X function.zip . -i index.js 'node_modules/*'"
},
"dependencies": {
"@yari-internal/constants": "file:../../../libs/constants"
},
"engines": {
"node": ">=12.x"
},
Expand Down
2 changes: 2 additions & 0 deletions deployer/aws-lambda/content-origin-response/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
# yarn lockfile v1


"@yari-internal/constants@file:../../../libs/constants":
version "0.0.1"
68 changes: 68 additions & 0 deletions libs/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,79 @@ const ACTIVE_LOCALES = new Set([
"zh-tw",
]);

const scriptSrcValues = [
"'report-sample'",
"'self'",
"*.speedcurve.com",
"'sha256-q7cJjDqNO2e1L5UltvJ1LhvnYN7yJXgGO7b6h9xkL1o='", // LUX
"www.google-analytics.com/analytics.js",
"'sha256-JEt9Nmc3BP88wxuTZm9aKNu87vEgGmKW1zzy/vb1KPs='", // polyfill check
"polyfill.io/v3/polyfill.min.js",
];
const CSP_DIRECTIVES = {
"default-src": ["'self'"],
"script-src": scriptSrcValues,
"script-src-elem": scriptSrcValues,
"style-src": ["'report-sample'", "'self'", "'unsafe-inline'"],
"object-src": ["'none'"],
"base-uri": ["'self'"],
"connect-src": ["'self'"],
"font-src": ["'self'"],
"frame-src": [
"'self'",
"interactive-examples.mdn.mozilla.net",
"mdn.github.io",
"yari-demos.prod.mdn.mozit.cloud",
"mdn.mozillademos.org",
"yari-demos.stage.mdn.mozit.cloud",
"jsfiddle.net",
"www.youtube-nocookie.com",
],
"img-src": [
"'self'",
"*.githubusercontent.com",
"*.googleusercontent.com",
"lux.speedcurve.com",
"mdn.mozillademos.org",
"media.prod.mdn.mozit.cloud",
"media.stage.mdn.mozit.cloud",
"interactive-examples.mdn.mozilla.net",
"wikipedia.org",
],
"manifest-src": ["'self'"],
"media-src": ["'self'", "archive.org", "videos.cdn.mozilla.net"],
"worker-src": ["'none'"],
"report-uri": ["/csp-violation-capture"],
};

const cspToString = (csp) =>
Object.entries(csp)
.map(([directive, values]) => `${directive} ${values.join(" ")};`)
.join(" ");

const CSP_VALUE_PROD = cspToString({
...CSP_DIRECTIVES,
"report-uri": [
"https://sentry.prod.mozaws.net/api/72/security/?sentry_key=25e652a045b642dfaa310e92e800058a",
],
});
const CSP_VALUE_STAGE = cspToString({
...CSP_DIRECTIVES,
"report-uri": [
"https://sentry.prod.mozaws.net/api/73/security/?sentry_key=8664389dc16c4e9786e4a396f2964952",
],
});
const CSP_VALUE_DEV = cspToString(CSP_DIRECTIVES);

module.exports = {
ACTIVE_LOCALES,
VALID_LOCALES,
RETIRED_LOCALES,
DEFAULT_LOCALE,
LOCALE_ALIASES,
PREFERRED_LOCALE_COOKIE_NAME,

CSP_VALUE_PROD,
CSP_VALUE_STAGE,
CSP_VALUE_DEV,
};
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@
"jest-junit-reporter": "^1.1.0",
"jest-puppeteer": "5.0.3",
"jsdom": "^16.5.3",
"jsesc": "^3.0.2",
"nodemon": "2.0.7",
"pegjs": "^0.10.0",
"prettier": "2.3.0",
Expand Down
19 changes: 19 additions & 0 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const {
} = require("../content");
// eslint-disable-next-line node/no-missing-require
const { prepareDoc, renderDocHTML } = require("../ssr/dist/main");
const { CSP_VALUE_DEV } = require("../libs/constants");

const { STATIC_ROOT, PROXY_HOSTNAME, FAKE_V1_API } = require("./constants");
const documentRouter = require("./document");
Expand Down Expand Up @@ -90,6 +91,23 @@ app.post("/:locale/users/account/signup", proxy);
// See https://github.com/chimurai/http-proxy-middleware/issues/40#issuecomment-163398924
app.use(express.urlencoded({ extended: true }));

app.post(
"/csp-violation-capture",
express.json({ type: "application/csp-report" }),
(req, res) => {
const report = req.body["csp-report"];
console.warn(
chalk.yellow(
"CSP violation for directive",
report["violated-directive"],
"which blocked:",
report["blocked-uri"]
)
);
res.sendStatus(200);
}
);

app.use("/_document", documentRouter);

app.get("/_open", (req, res) => {
Expand Down Expand Up @@ -263,6 +281,7 @@ app.get("/*", async (req, res) => {
if (isJSONRequest) {
res.json({ doc: document });
} else {
res.header("Content-Security-Policy", CSP_VALUE_DEV);
res.send(renderDocHTML(document, lookupURL));
}
});
Expand Down
10 changes: 9 additions & 1 deletion server/middlewares.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const express = require("express");

const { CSP_VALUE_DEV } = require("../libs/constants");
const { resolveFundamental } = require("../libs/fundamental-redirects");
const { getLocale } = require("../libs/get-locale");
const { STATIC_ROOT } = require("./constants");
Expand Down Expand Up @@ -48,6 +49,13 @@ const originRequest = (req, res, next) => {
};

module.exports = {
staticMiddlewares: [slugRewrite, express.static(STATIC_ROOT)],
staticMiddlewares: [
slugRewrite,
express.static(STATIC_ROOT, {
setHeaders: (res) => {
res.setHeader("Content-Security-Policy", CSP_VALUE_DEV);
},
}),
],
originRequestMiddleware: originRequest,
};
39 changes: 14 additions & 25 deletions ssr/render.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import fs from "fs";
import path from "path";

import jsesc from "jsesc";
import { renderToString } from "react-dom/server";
import cheerio from "cheerio";

import {
SPEEDCURVE_LUX_ID,
ALWAYS_NO_ROBOTS,
BUILD_OUT_ROOT,
SPEEDCURVE_LUX_ID,
} from "../build/constants";

const { DEFAULT_LOCALE } = require("../libs/constants");

// When there are multiple options for a given language, this gives the
Expand Down Expand Up @@ -130,13 +129,6 @@ function* extractCSSURLs(css, filterFunction) {
}
}

function serializeDocumentData(data) {
return jsesc(JSON.stringify(data), {
json: true,
isScriptContext: true,
});
}

export default function render(
renderApp,
{
Expand Down Expand Up @@ -165,15 +157,12 @@ export default function render(

let pageDescription = "";

const hydrationData = {};
if (pageNotFound) {
pageTitle = `🤷🏽‍♀️ Page not found | ${pageTitle}`;
const documentDataTag = `<script>window.__pageNotFound__ = true;</script>`;
$("#root").after(documentDataTag);
hydrationData.pageNotFound = true;
} else if (feedEntries) {
const feedEntriesTag = `<script>window.__feedEntries__ = JSON.parse(${serializeDocumentData(
feedEntries
)});</script>`;
$("#root").after(feedEntriesTag);
hydrationData.feedEntries = feedEntries;
} else if (doc) {
// Use the doc's title instead
pageTitle = doc.pageTitle;
Expand All @@ -183,10 +172,7 @@ export default function render(
pageDescription = doc.summary;
}

const documentDataTag = `<script>window.__data__ = JSON.parse(${serializeDocumentData(
doc
)});</script>`;
$("#root").after(documentDataTag);
hydrationData.doc = doc;

if (doc.other_translations) {
const allOtherLocales = doc.other_translations.map((t) => t.locale);
Expand Down Expand Up @@ -215,12 +201,15 @@ export default function render(
}

if (possibleLocales) {
const possibleLocalesTag = `<script>window.__possibleLocales__ = JSON.parse(${serializeDocumentData(
possibleLocales
)});</script>`;
$("#root").after(possibleLocalesTag);
hydrationData.possibleLocales = possibleLocales;
}

$("#root").after(
`<script type="application/json" id="hydration">${JSON.stringify(
hydrationData
)}</script>`
);

if (pageDescription) {
// This overrides the default description. Also assumes there's always
// one tag there already.
Expand All @@ -244,7 +233,7 @@ export default function render(
// The snippet is always the same, if it's present, but the ID varies
// See LUX settings here https://speedcurve.com/mozilla-add-ons/mdn/settings/lux/
const speedcurveJS = getSpeedcurveJS();
$("<script>").text(`\n${speedcurveJS}\n`).appendTo($("head"));
$("<script>").text(speedcurveJS).appendTo($("head"));
$(
`<script src="https://cdn.speedcurve.com/js/lux.js?id=${SPEEDCURVE_LUX_ID}" async defer crossorigin="anonymous"></script>`
).appendTo($("head"));
Expand Down
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12472,11 +12472,6 @@ jsesc@^2.5.1:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==

jsesc@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e"
integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==

jsesc@~0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
Expand Down

0 comments on commit 107cf0e

Please sign in to comment.