From 5a6f5f064b9f1e68dbf502fe7e25859262ea9f1e Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 28 Sep 2023 03:00:22 +0200 Subject: [PATCH] Security: add inline script with automatic hash --- package-lock.json | 16 +++++++++++-- package.json | 10 ++++---- scripts/generateLayouts.ts | 39 ++++++++++++++++++++++++++------ scripts/hetzner/config/Caddyfile | 4 ++++ scripts/hetzner/deployHetzner.sh | 4 ++-- src/UI/RemoveOtherLanguages.js | 31 +++++++++++++++++++++++++ src/UI/RemoveOtherLanguages.ts | 32 -------------------------- theme.html | 2 +- 8 files changed, 89 insertions(+), 49 deletions(-) create mode 100644 src/UI/RemoveOtherLanguages.js delete mode 100644 src/UI/RemoveOtherLanguages.ts diff --git a/package-lock.json b/package-lock.json index 5f6a1ce4f..696938687 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapcomplete", - "version": "0.33.1", + "version": "0.33.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mapcomplete", - "version": "0.33.1", + "version": "0.33.5", "license": "GPL-3.0-or-later", "dependencies": { "@rgossiaux/svelte-headlessui": "^1.0.2", @@ -23,6 +23,7 @@ "chart.js": "^3.8.0", "country-language": "^0.1.7", "country-to-currency": "^1.0.10", + "crypto": "^1.0.1", "csv-parse": "^5.1.0", "doctest-ts-improved": "^0.8.8", "dompurify": "^3.0.5", @@ -5392,6 +5393,12 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in." + }, "node_modules/css-line-break": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", @@ -17341,6 +17348,11 @@ "which": "^2.0.1" } }, + "crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==" + }, "css-line-break": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", diff --git a/package.json b/package.json index 98b48288c..45ede4ec4 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "main": "index.ts", "type": "module", "config": { - "#": "Various endpoints that are instance-specific. This is the default configuration, which is re-exported in 'Constants.ts'.", "#": "Use MAPCOMPLETE_CONFIGURATION to use an additional configuration, e.g. `MAPCOMPLETE_CONFIGURATION=config_hetzner`", "#oauth_credentials:comment": [ "`oauth_credentials` are the OAuth-2 credentials for the production-OSM server and the test-server.", @@ -18,10 +17,10 @@ "Alternatively, you can override the `osm` credentials using the environment variables `VITE_OSM_OAUTH_CLIENT_ID` and `VITE_OSM_OAUTH_SECRET`" ], "oauth_credentials": { - "#": "This client-id is registered by 'MapComplete' on osm.org", - "oauth_client_id": "K93H1d8ve7p-tVLE1ZwsQ4lAFLQk8INx5vfTLMu5DWk", - "oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg", - "url": "https://www.openstreetmap.org" + "#": "This client-id is registered by 'MapComplete' on osm.org", + "oauth_client_id": "K93H1d8ve7p-tVLE1ZwsQ4lAFLQk8INx5vfTLMu5DWk", + "oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg", + "url": "https://www.openstreetmap.org" }, "api_keys": { "#": "Various API-keys for various services. Feel free to reuse those in another MapComplete-hosted version", @@ -108,6 +107,7 @@ "chart.js": "^3.8.0", "country-language": "^0.1.7", "country-to-currency": "^1.0.10", + "crypto": "^1.0.1", "csv-parse": "^5.1.0", "doctest-ts-improved": "^0.8.8", "dompurify": "^3.0.5", diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index 4a8086728..83103f7c3 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -12,6 +12,7 @@ import SpecialVisualizations from "../src/UI/SpecialVisualizations" import Constants from "../src/Models/Constants" import { AvailableRasterLayers, RasterLayerPolygon } from "../src/Models/RasterLayers" import { ImmutableStore } from "../src/Logic/UIEventSource" +import * as crypto from "crypto" const sharp = require("sharp") const template = readFileSync("theme.html", "utf8") @@ -205,9 +206,14 @@ function asLangSpan(t: Translation, tag = "span"): string { } let previousSrc: Set = new Set() -function generateCsp(layout: LayoutConfig): string { +function generateCsp( + layout: LayoutConfig, + options: { + scriptSrcs: string[] + } +): string { const apiUrls: string[] = [ - "self", + "'self'", ...Constants.defaultOverpassUrls, Constants.countryCoderEndpoint, "https://api.openstreetmap.org", @@ -248,9 +254,11 @@ function generateCsp(layout: LayoutConfig): string { ) previousSrc = hosts - const csp = { + const csp: Record = { "default-src": "'self'", - "script-src": "'self' https://gc.zgo.at/count.js", + "script-src": ["'self'", "https://gc.zgo.at/count.js", ...(options?.scriptSrcs ?? [])].join( + " " + ), "img-src": "* data:", // maplibre depends on 'data:' to load "connect-src": connectSrc.join(" "), "report-to": "https://report.mapcomplete.org/csp", @@ -267,6 +275,14 @@ function generateCsp(layout: LayoutConfig): string { ].join("\n") } +const removeOtherLanguages = readFileSync("./src/UI/RemoveOtherLanguages.js", "utf8") + .split("\n") + .map((s) => s.trim()) + .join("\n") +const removeOtherLanguagesHash = crypto + .createHash("sha256") + .update(removeOtherLanguages) + .digest("base64") async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) { Locale.language.setData(layout.language[0]) const targetLanguage = layout.language[0] @@ -338,7 +354,10 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr ].join("\n") const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title }) - + const templateLines = template.split("\n") + const removeOtherLanguagesReference = templateLines.find( + (line) => line.indexOf("./src/UI/RemoveOtherLanguages.js") >= 0 + ) let output = template .replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1")) .replace( @@ -346,7 +365,13 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr Translations.t.general.poweredByOsm.textFor(targetLanguage) ) .replace(/.*/s, themeSpecific) - .replace(//, generateCsp(layout)) + .replace( + //, + generateCsp(layout, { + scriptSrcs: [`'sha256-${removeOtherLanguagesHash}'`], + }) + ) + .replace(removeOtherLanguagesReference, "") .replace( /.*/s, asLangSpan(layout.shortDescription) @@ -357,7 +382,7 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr ) .replace( - '', + /.*\/src\/index\.ts.*/, `` ) diff --git a/scripts/hetzner/config/Caddyfile b/scripts/hetzner/config/Caddyfile index 1638f7541..27b328008 100644 --- a/scripts/hetzner/config/Caddyfile +++ b/scripts/hetzner/config/Caddyfile @@ -10,6 +10,10 @@ hosted.mapcomplete.org { countrycoder.mapcomplete.org { root * tiles/ file_server + header { + +Permissions-Policy "interest-cohort=()" + +Access-Control-Allow-Origin https://hosted.mapcomplete.org https://dev.mapcomplete.org https://mapcomplete.org + } } diff --git a/scripts/hetzner/deployHetzner.sh b/scripts/hetzner/deployHetzner.sh index 587a5fddb..95fab61c1 100755 --- a/scripts/hetzner/deployHetzner.sh +++ b/scripts/hetzner/deployHetzner.sh @@ -17,8 +17,8 @@ npm run test npm run prepare-deploy && mv config.json.bu config.json && zip dist.zip -r dist/* && -scp -r dist.zip hetzner:/root/ && -echo "Upload completed, deploying config and booting" && +scp ./scripts/hetzner/config/* hetzner:/root/ && rsync -rzh --progress dist.zip hetzner:/root/ && +echo "Upload completed, deploying config and booting" && ssh hetzner -t "unzip dist.zip && rm dist.zip && rm -rf public/ && mv dist public && caddy stop && caddy start" && rm dist.zip diff --git a/src/UI/RemoveOtherLanguages.js b/src/UI/RemoveOtherLanguages.js new file mode 100644 index 000000000..7486047ca --- /dev/null +++ b/src/UI/RemoveOtherLanguages.js @@ -0,0 +1,31 @@ +let lang = ( + (navigator.languages && navigator.languages[0]) || + navigator.language || + navigator["userLanguage"] || + "en" +).substr(0, 2) + +function filterLangs(maindiv) { + let foundLangs = 0 + for (const child of Array.from(maindiv.children)) { + if (child.attributes.getNamedItem("lang")?.value === lang) { + foundLangs++ + } + } + if (foundLangs === 0) { + lang = "en" + } + for (const child of Array.from(maindiv.children)) { + const childLang = child.attributes.getNamedItem("lang") + if (childLang === undefined) { + continue + } + if (childLang.value === lang) { + continue + } + child.parentElement.removeChild(child) + } +} + +filterLangs(document.getElementById("descriptions-while-loading")) +filterLangs(document.getElementById("default-title")) diff --git a/src/UI/RemoveOtherLanguages.ts b/src/UI/RemoveOtherLanguages.ts deleted file mode 100644 index f321df3e5..000000000 --- a/src/UI/RemoveOtherLanguages.ts +++ /dev/null @@ -1,32 +0,0 @@ -export {} -let lang = ( - (navigator.languages && navigator.languages[0]) || - navigator.language || - navigator["userLanguage"] || - "en" -).substr(0, 2) - -function filterLangs(maindiv: HTMLElement) { - let foundLangs = 0 - for (const child of Array.from(maindiv.children)) { - if (child.attributes.getNamedItem("lang")?.value === lang) { - foundLangs++ - } - } - if (foundLangs === 0) { - lang = "en" - } - for (const child of Array.from(maindiv.children)) { - const childLang = child.attributes.getNamedItem("lang") - if (childLang === undefined) { - continue - } - if (childLang.value === lang) { - continue - } - child.parentElement.removeChild(child) - } -} - -filterLangs(document.getElementById("descriptions-while-loading")) -filterLangs(document.getElementById("default-title")) diff --git a/theme.html b/theme.html index 362218168..3aab458b1 100644 --- a/theme.html +++ b/theme.html @@ -65,7 +65,7 @@
Below
- +