Security: add inline script with automatic hash
This commit is contained in:
parent
4852888b41
commit
5a6f5f064b
8 changed files with 89 additions and 49 deletions
16
package-lock.json
generated
16
package-lock.json
generated
|
@ -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",
|
||||
|
|
10
package.json
10
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",
|
||||
|
|
|
@ -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>`
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
31
src/UI/RemoveOtherLanguages.js
Normal file
31
src/UI/RemoveOtherLanguages.js
Normal 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"))
|
|
@ -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"))
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue