Security: add inline script with automatic hash

This commit is contained in:
Pieter Vander Vennet 2023-09-28 03:00:22 +02:00
parent 4852888b41
commit 5a6f5f064b
8 changed files with 89 additions and 49 deletions

16
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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<string> = new Set<string>()
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<string, string> = {
"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(/<!-- THEME-SPECIFIC -->.*<!-- THEME-SPECIFIC-END-->/s, themeSpecific)
.replace(/<!-- CSP -->/, generateCsp(layout))
.replace(
/<!-- CSP -->/,
generateCsp(layout, {
scriptSrcs: [`'sha256-${removeOtherLanguagesHash}'`],
})
)
.replace(removeOtherLanguagesReference, "<script>" + removeOtherLanguages + "</script>")
.replace(
/<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s,
asLangSpan(layout.shortDescription)
@ -357,7 +382,7 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
)
.replace(
'<script src="./src/index.ts" type="module"></script>',
/.*\/src\/index\.ts.*/,
`<script type="module" src="./index_${layout.id}.ts"></script>`
)

View file

@ -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
}
}

View file

@ -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

View file

@ -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"))

View file

@ -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"))

View file

@ -65,7 +65,7 @@
</div>
</div>
<div id="belowmap" class="absolute top-0 left-0 -z-10">Below</div>
<script async src="./src/UI/RemoveOtherLanguages.ts" type="module"></script>
<script src="./src/UI/RemoveOtherLanguages.js"></script>
<script async src="./src/InstallServiceWorker.ts" type="module"></script>
<script defer src="./src/index.ts" type="module"></script>
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="https://gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script>