forked from MapComplete/MapComplete
		
	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", | ||||
|  |  | |||
|  | @ -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.", | ||||
|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue