source: Klonkt/test/csp-no-swapped-scripts.test.js

main
Last change on this file was 59f404a, checked in by Robin <roboburr@…>, 4 days ago

De laatste twee scripts uit gewisselde inhoud (shaer-0i6)

shaer-bqr haalde de eigen logica van paid-gate en paid-passkey al uit de
pagina naar een module, maar liet de WebAuthn-bibliotheek als losse tag in
de body staan. Beide pagina's gaan via renderPage, dus bij een navigatie
binnen de site komen ze als fragment binnen -- met de nonce van dat
verzoek, die het al geladen document niet kent. Precies de vorm die Bart
destijds gemeld zag.

Het gevolg was stil: de module laadde wel, vond window.SimpleWebAuthn-
Browser niet, en viel terug op "geen WebAuthn hier". De passkey-knop deed
niets; de ontgrendelknop verdween. Alleen na een harde herlading werkte
het, en dat is precies waarom dit lang onzichtbaar bleef.

De modules halen de bibliotheek nu zelf op met loadWebAuthn() in lib.js.
Een dynamische import vanuit een module die de shell met een geldige nonce
startte is toegestaan -- daar is strict-dynamic voor. De UMD-bundel zet
zichzelf zonder CommonJS of AMD op globalThis, dus de global staat er na
afloop. init() wacht erop voordat hij bedraadt, anders zet de terugval de
knop uit terwijl er niets aan de hand is.

Verder nagelopen wat nog meer een scripttag draagt: vijf views hebben er
een van type application/json (een datablok, dat voert de browser nooit
uit) en twee gaan via res.render in plaats van renderPage -- losse
documenten die htmx nooit inwisselt. Die staan als uitzondering in de
nieuwe toets, met een tweede toets die controleert dat ze nog bestaan,
zodat een hernoeming de uitzondering niet stilletjes uitholt.

MOD_V naar 63: er wijzigde iets in mod/.

  • Property mode set to 100644
File size: 3.0 KB
Line 
1// Geen uitvoerbaar script in inhoud die htmx wisselt (shaer-0i6).
2//
3// De CSP-nonce rouleert per verzoek. Een pagina die je via een link BINNEN de
4// site opent komt als fragment binnen, met de nonce van dat verzoek -- en die
5// kent het al geladen document niet, dus wordt zo'n script geweigerd. Elk
6// paginascript hoort daarom uit de shell te komen, die op body[data-js] een
7// module importeert; de gegevens gaan via partials/page-data.ejs.
8//
9// De valkuil die dit bewaakt: de fout is ONZICHTBAAR na een herlading. Wie een
10// script terugzet in een view merkt daar bij het testen niets van, tenzij hij
11// er via de navigatie naartoe gaat. Deze toets merkt het wel.
12import { test } from 'node:test';
13import assert from 'node:assert/strict';
14import fs from 'node:fs';
15import path from 'node:path';
16import { fileURLToPath } from 'node:url';
17
18const VIEWS = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'views');
19
20// Losse DOCUMENTEN, geen fragmenten: deze gaan via res.render en niet via
21// renderPage, dus htmx wisselt ze nooit in en hun scripts komen altijd met de
22// nonce van hun eigen laadbeurt binnen. Wie hier iets aan toevoegt moet dus
23// eerst nagaan langs welke weg de pagina gerenderd wordt.
24const LOSSE_DOCUMENTEN = new Set(['pages/embed-player.ejs', 'pages/guardian.ejs']);
25
26function views() {
27 const uit = [];
28 for (const map of ['pages', 'partials']) {
29 for (const naam of fs.readdirSync(path.join(VIEWS, map))) {
30 if (naam.endsWith('.ejs')) uit.push(`${map}/${naam}`);
31 }
32 }
33 return uit.sort();
34}
35
36test('geen uitvoerbaar script in een view die als fragment binnenkomt', () => {
37 const overtreders = [];
38 for (const rel of views()) {
39 if (LOSSE_DOCUMENTEN.has(rel)) continue;
40 // EJS-commentaar eerst weg: dat komt nooit in de uitvoer, en de views
41 // leggen juist in zo'n commentaar uit welke scripttag daar wegging. Zonder
42 // dit sloeg deze toets aan op zijn eigen uitleg.
43 const bron = fs.readFileSync(path.join(VIEWS, rel), 'utf8').replace(/<%#[\s\S]*?%>/g, '');
44 for (const m of bron.matchAll(/<script\b[^>]*>/gi)) {
45 // Een datablok (application/json) wordt door de browser nooit
46 // uitgevoerd, dus daar gaat script-src niet over. Dat is de vorm
47 // waarin een pagina zijn gegevens wel mag meegeven.
48 if (/type\s*=\s*["']application\/json["']/i.test(m[0])) continue;
49 overtreders.push(`${rel}: ${m[0].slice(0, 80)}`);
50 }
51 }
52 assert.deepEqual(overtreders, [],
53 'deze scripts overleven een htmx-navigatie niet; laat de shell de module laden (zie shaer-0i6)');
54});
55
56test('de losse documenten bestaan nog, anders bewaakt de uitzondering niets', () => {
57 // Een uitzonderingslijst die naar verdwenen bestanden wijst maakt de toets
58 // stilletjes zwakker: hernoemt iemand zo'n pagina, dan valt hij ineens onder
59 // de regel zonder dat iemand dat besloot.
60 for (const rel of LOSSE_DOCUMENTEN) {
61 assert.ok(fs.existsSync(path.join(VIEWS, rel)), `${rel} staat op de uitzonderingslijst maar bestaat niet meer`);
62 }
63});
Note: See TracBrowser for help on using the repository browser.