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", "name": "mapcomplete",
"version": "0.33.1", "version": "0.33.5",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mapcomplete", "name": "mapcomplete",
"version": "0.33.1", "version": "0.33.5",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"dependencies": { "dependencies": {
"@rgossiaux/svelte-headlessui": "^1.0.2", "@rgossiaux/svelte-headlessui": "^1.0.2",
@ -23,6 +23,7 @@
"chart.js": "^3.8.0", "chart.js": "^3.8.0",
"country-language": "^0.1.7", "country-language": "^0.1.7",
"country-to-currency": "^1.0.10", "country-to-currency": "^1.0.10",
"crypto": "^1.0.1",
"csv-parse": "^5.1.0", "csv-parse": "^5.1.0",
"doctest-ts-improved": "^0.8.8", "doctest-ts-improved": "^0.8.8",
"dompurify": "^3.0.5", "dompurify": "^3.0.5",
@ -5392,6 +5393,12 @@
"node": ">= 8" "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": { "node_modules/css-line-break": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
@ -17341,6 +17348,11 @@
"which": "^2.0.1" "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": { "css-line-break": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",

View file

@ -8,7 +8,6 @@
"main": "index.ts", "main": "index.ts",
"type": "module", "type": "module",
"config": { "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`", "#": "Use MAPCOMPLETE_CONFIGURATION to use an additional configuration, e.g. `MAPCOMPLETE_CONFIGURATION=config_hetzner`",
"#oauth_credentials:comment": [ "#oauth_credentials:comment": [
"`oauth_credentials` are the OAuth-2 credentials for the production-OSM server and the test-server.", "`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`" "Alternatively, you can override the `osm` credentials using the environment variables `VITE_OSM_OAUTH_CLIENT_ID` and `VITE_OSM_OAUTH_SECRET`"
], ],
"oauth_credentials": { "oauth_credentials": {
"#": "This client-id is registered by 'MapComplete' on osm.org", "#": "This client-id is registered by 'MapComplete' on osm.org",
"oauth_client_id": "K93H1d8ve7p-tVLE1ZwsQ4lAFLQk8INx5vfTLMu5DWk", "oauth_client_id": "K93H1d8ve7p-tVLE1ZwsQ4lAFLQk8INx5vfTLMu5DWk",
"oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg", "oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg",
"url": "https://www.openstreetmap.org" "url": "https://www.openstreetmap.org"
}, },
"api_keys": { "api_keys": {
"#": "Various API-keys for various services. Feel free to reuse those in another MapComplete-hosted version", "#": "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", "chart.js": "^3.8.0",
"country-language": "^0.1.7", "country-language": "^0.1.7",
"country-to-currency": "^1.0.10", "country-to-currency": "^1.0.10",
"crypto": "^1.0.1",
"csv-parse": "^5.1.0", "csv-parse": "^5.1.0",
"doctest-ts-improved": "^0.8.8", "doctest-ts-improved": "^0.8.8",
"dompurify": "^3.0.5", "dompurify": "^3.0.5",

View file

@ -12,6 +12,7 @@ import SpecialVisualizations from "../src/UI/SpecialVisualizations"
import Constants from "../src/Models/Constants" import Constants from "../src/Models/Constants"
import { AvailableRasterLayers, RasterLayerPolygon } from "../src/Models/RasterLayers" import { AvailableRasterLayers, RasterLayerPolygon } from "../src/Models/RasterLayers"
import { ImmutableStore } from "../src/Logic/UIEventSource" import { ImmutableStore } from "../src/Logic/UIEventSource"
import * as crypto from "crypto"
const sharp = require("sharp") const sharp = require("sharp")
const template = readFileSync("theme.html", "utf8") const template = readFileSync("theme.html", "utf8")
@ -205,9 +206,14 @@ function asLangSpan(t: Translation, tag = "span"): string {
} }
let previousSrc: Set<string> = new Set<string>() let previousSrc: Set<string> = new Set<string>()
function generateCsp(layout: LayoutConfig): string { function generateCsp(
layout: LayoutConfig,
options: {
scriptSrcs: string[]
}
): string {
const apiUrls: string[] = [ const apiUrls: string[] = [
"self", "'self'",
...Constants.defaultOverpassUrls, ...Constants.defaultOverpassUrls,
Constants.countryCoderEndpoint, Constants.countryCoderEndpoint,
"https://api.openstreetmap.org", "https://api.openstreetmap.org",
@ -248,9 +254,11 @@ function generateCsp(layout: LayoutConfig): string {
) )
previousSrc = hosts previousSrc = hosts
const csp = { const csp: Record<string, string> = {
"default-src": "'self'", "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 "img-src": "* data:", // maplibre depends on 'data:' to load
"connect-src": connectSrc.join(" "), "connect-src": connectSrc.join(" "),
"report-to": "https://report.mapcomplete.org/csp", "report-to": "https://report.mapcomplete.org/csp",
@ -267,6 +275,14 @@ function generateCsp(layout: LayoutConfig): string {
].join("\n") ].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) { async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) {
Locale.language.setData(layout.language[0]) Locale.language.setData(layout.language[0])
const targetLanguage = layout.language[0] const targetLanguage = layout.language[0]
@ -338,7 +354,10 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
].join("\n") ].join("\n")
const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title }) 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 let output = template
.replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1")) .replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1"))
.replace( .replace(
@ -346,7 +365,13 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
Translations.t.general.poweredByOsm.textFor(targetLanguage) Translations.t.general.poweredByOsm.textFor(targetLanguage)
) )
.replace(/<!-- THEME-SPECIFIC -->.*<!-- THEME-SPECIFIC-END-->/s, themeSpecific) .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( .replace(
/<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s, /<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s,
asLangSpan(layout.shortDescription) asLangSpan(layout.shortDescription)
@ -357,7 +382,7 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
) )
.replace( .replace(
'<script src="./src/index.ts" type="module"></script>', /.*\/src\/index\.ts.*/,
`<script type="module" src="./index_${layout.id}.ts"></script>` `<script type="module" src="./index_${layout.id}.ts"></script>`
) )

View file

@ -10,6 +10,10 @@ hosted.mapcomplete.org {
countrycoder.mapcomplete.org { countrycoder.mapcomplete.org {
root * tiles/ root * tiles/
file_server 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 && npm run prepare-deploy &&
mv config.json.bu config.json && mv config.json.bu config.json &&
zip dist.zip -r dist/* && zip dist.zip -r dist/* &&
scp -r dist.zip hetzner:/root/ && scp ./scripts/hetzner/config/* hetzner:/root/ &&
echo "Upload completed, deploying config and booting" &&
rsync -rzh --progress dist.zip 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" && ssh hetzner -t "unzip dist.zip && rm dist.zip && rm -rf public/ && mv dist public && caddy stop && caddy start" &&
rm dist.zip 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> </div>
<div id="belowmap" class="absolute top-0 left-0 -z-10">Below</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 async src="./src/InstallServiceWorker.ts" type="module"></script>
<script defer src="./src/index.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> <script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="https://gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script>