forked from MapComplete/MapComplete
		
	Fix: add script integrity, add check to validate that script integrity is always in place
This commit is contained in:
		
							parent
							
								
									3f0ca80117
								
							
						
					
					
						commit
						08bbbcabc4
					
				
					 6 changed files with 85 additions and 15 deletions
				
			
		
							
								
								
									
										2
									
								
								404.html
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								404.html
									
										
									
									
									
								
							|  | @ -50,7 +50,7 @@ | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| <script type="module" src="./src/notfound.ts"></script> | <script type="module" src="./src/notfound.ts"></script> | ||||||
| <script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js"></script> | <script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script> | ||||||
| 
 | 
 | ||||||
| </body> | </body> | ||||||
| </html> | </html> | ||||||
|  |  | ||||||
|  | @ -51,7 +51,8 @@ | ||||||
| 
 | 
 | ||||||
| <div id="main"></div> | <div id="main"></div> | ||||||
| <script type="module" src="./src/all_themes_index.ts"></script> | <script type="module" src="./src/all_themes_index.ts"></script> | ||||||
| <script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js"></script> | <script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js" crossorigin="anonymous" | ||||||
|  |         integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script> | ||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
|     window.addEventListener('load', () => { |     window.addEventListener('load', () => { | ||||||
|  |  | ||||||
							
								
								
									
										32
									
								
								scripts/generateRedirectFiles.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								scripts/generateRedirectFiles.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | ||||||
|  | import Script from "./Script" | ||||||
|  | import ScriptUtils from "./ScriptUtils" | ||||||
|  | import { writeFileSync } from "fs" | ||||||
|  | import { AllKnownLayouts } from "../src/Customizations/AllKnownLayouts" | ||||||
|  | 
 | ||||||
|  | class CreateRedirectFiles extends Script { | ||||||
|  |     constructor() { | ||||||
|  |         super( | ||||||
|  |             "Creates a redirect html-file in the 'mapcomplete-osm-be' repository for every .html file and every known theme" | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |     async main(args: string[]): Promise<void> { | ||||||
|  |         const htmlFiles = ScriptUtils.readDirRecSync(".", 1) | ||||||
|  |             .filter((f) => f.endsWith(".html")) | ||||||
|  |             .map((s) => s.substring(2, s.length - 5)) | ||||||
|  |         const themes = Array.from(AllKnownLayouts.allKnownLayouts.keys()) | ||||||
|  |         htmlFiles.push(...themes) | ||||||
|  |         console.log("HTML files are:", htmlFiles) | ||||||
|  |         for (const htmlFile of htmlFiles) { | ||||||
|  |             let path = "" | ||||||
|  |             if (htmlFile !== "index") { | ||||||
|  |                 path = htmlFile | ||||||
|  |             } | ||||||
|  |             writeFileSync( | ||||||
|  |                 "../mapcomplete-osm-be/" + htmlFile + ".html", | ||||||
|  |                 `<meta http-equiv="Refresh" content="0; url='https://mapcomplete.org/${path}'" />` | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | new CreateRedirectFiles().run() | ||||||
|  | @ -14,7 +14,7 @@ | ||||||
| <body> | <body> | ||||||
| <div id="main">Loading statistics...</div> | <div id="main">Loading statistics...</div> | ||||||
| <script src="./src/UI/StatisticsGUI.ts" type="module"></script> | <script src="./src/UI/StatisticsGUI.ts" type="module"></script> | ||||||
| <script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js"></script> | <script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script> | ||||||
| 
 | 
 | ||||||
| </body> | </body> | ||||||
| </html> | </html> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,10 @@ | ||||||
| import { exec } from "child_process" | import { exec } from "child_process" | ||||||
| import { describe, it } from "vitest" | import { describe, it } from "vitest" | ||||||
| 
 | 
 | ||||||
|  | import { parse as parse_html } from "node-html-parser" | ||||||
|  | import { readFileSync } from "fs" | ||||||
|  | import ScriptUtils from "../scripts/ScriptUtils" | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * |  * | ||||||
|  * @param forbidden: a GREP-regex. This means that '.' is a wildcard and should be escaped to match a literal dot |  * @param forbidden: a GREP-regex. This means that '.' is a wildcard and should be escaped to match a literal dot | ||||||
|  | @ -64,6 +68,32 @@ function itAsync(name: string, promise: Promise<void>) { | ||||||
|     it(name, wrap(promise)) |     it(name, wrap(promise)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function validateScriptIntegrityOf(path: string) { | ||||||
|  |     const htmlContents = readFileSync(path, "utf8") | ||||||
|  |     const doc = parse_html(htmlContents) | ||||||
|  |     // @ts-ignore
 | ||||||
|  |     const scripts = Array.from(doc.getElementsByTagName("script")) | ||||||
|  |     for (const script of scripts) { | ||||||
|  |         const src = script.getAttribute("src") | ||||||
|  |         if (src === undefined) { | ||||||
|  |             continue | ||||||
|  |         } | ||||||
|  |         if (src.startsWith("./")) { | ||||||
|  |             // Local script - no check needed
 | ||||||
|  |             continue | ||||||
|  |         } | ||||||
|  |         const integrity = script.getAttribute("integrity") | ||||||
|  |         const ctx = "Script with source " + src + " in file " + path | ||||||
|  |         if (integrity === undefined) { | ||||||
|  |             throw new Error(ctx + " has no integrity value") | ||||||
|  |         } | ||||||
|  |         const crossorigin = script.getAttribute("crossorigin") | ||||||
|  |         if (crossorigin !== "anonymous") { | ||||||
|  |             throw new Error(ctx + " has crossorigin missing or not set to 'anonymous'") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| describe("Code quality", () => { | describe("Code quality", () => { | ||||||
|     itAsync( |     itAsync( | ||||||
|         "should not contain reverse", |         "should not contain reverse", | ||||||
|  | @ -85,17 +115,24 @@ describe("Code quality", () => { | ||||||
|             "innerText is not allowed as it is not testable with fakeDom. Use 'textContent' instead." |             "innerText is not allowed as it is not testable with fakeDom. Use 'textContent' instead." | ||||||
|         ) |         ) | ||||||
|     ) |     ) | ||||||
|  | 
 | ||||||
|  |     it("scripts with external sources should have an integrity hash", () => { | ||||||
|  |         const htmlFiles = ScriptUtils.readDirRecSync(".", 1).filter((f) => f.endsWith(".html")) | ||||||
|  |         for (const htmlFile of htmlFiles) { | ||||||
|  |             validateScriptIntegrityOf(htmlFile) | ||||||
|  |         } | ||||||
|  |     }) | ||||||
|     /* |     /* | ||||||
|     itAsync( |   itAsync( | ||||||
|         "should not contain 'import * as name from \"xyz.json\"'", |       "should not contain 'import * as name from \"xyz.json\"'", | ||||||
|         detectInCode( |       detectInCode( | ||||||
|             'import \\* as [a-zA-Z0-9_]\\+ from \\"[.-_/a-zA-Z0-9]\\+\\.json\\"', |           'import \\* as [a-zA-Z0-9_]\\+ from \\"[.-_/a-zA-Z0-9]\\+\\.json\\"', | ||||||
|             "With vite, json files have a default export. Use import name from file.json instead" |           "With vite, json files have a default export. Use import name from file.json instead" | ||||||
|         ) |       ) | ||||||
|     ) |   ) | ||||||
| /* | /* | ||||||
|     itAsync( |   itAsync( | ||||||
|         "should not contain '[\"default\"]'", |       "should not contain '[\"default\"]'", | ||||||
|         detectInCode('\\[\\"default\\"\\]', "Possible leftover of faulty default import") |       detectInCode('\\[\\"default\\"\\]', "Possible leftover of faulty default import") | ||||||
|     )*/ |   )*/ | ||||||
| }) | }) | ||||||
|  |  | ||||||
|  | @ -97,7 +97,7 @@ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| <script src="./src/index.ts" type="module"></script> | <script src="./src/index.ts" type="module"></script> | ||||||
| <script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js"></script> | <script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js" crossorigin="anonymous" integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script> | ||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
|     window.addEventListener('load', () => { |     window.addEventListener('load', () => { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue