#

Subresource integrity check u javascriptových modulů




TD;DR verze Javascriptové moduly nemají nástroj jak zkontrolovat integritu souboru vkládaného do vaší stránky. Bezpečnost se ale i tak dá zajistit pomocí směsice unikátního chování modulů, modul se totiž nikdy nevkládá 2x a v případě že první pokus o vložení selže na základě špatné integrity, druhý pokus se již neprovádí. Funguje jak pro klasický tak dynamický import modulu.

Javascriptové moduly? Co to je?

Tento článek pojednává o tom, jak javascriptové moduly zabezpečit pomocí subresource integrity, o tom, co to vlastně je ten modul si můžete přečíst v dnešním důkladně zpracovaný článek na Vzhůru dolů.

Proč? Aneb motivace.

Sice to všichni víme, ale malé opáčko: HTML stránka od počátku standardů do současnosti (kdy už máme jen 1 standard :) ) může skládat z více nezávislých částí. Obrázky, scripty, css, všechno mohou být samostatné soubory, klidně z jiné domény, kterou nemám pod svou správou. Tady právě může nastat problém, když z cizího zdroje vložím něco co potřebuji, může se stát, že za nějaký čas se na tom samém umístění objeví škodlivý kód a já ho stále vkládám a nemám ani nejmenší tušení, co se děje. Stejně tak může škodlivý kód být nějak zapodmínkovaný (třeba zobrazit škodlivý kód podle hlavičky jen uživatelům se starším prohlížečem, který je možné napadnout). A změnu součástí může provést také někdo kdo má přístup ke komunikaci, v případě nešifrovaného spojení… to už se vlastně v dnešní době stát nemůže, tak nic. Ale změnu zdrojů vám může ve vašem PC provést různý malware či nebezpečný plugin do prohlížeče. A že se to skutečně stává, útočníci takto rádi buď integrují těžbu kryptoměn nebo jen změní id toho kdo inkasuje peníze za reklamy na webu, případně vloží spoustu nových dalších reklam, které by na webu jinak neměli co dělat. Možnosti zneužití jsou samozřejmě neomezené a jak se říká fantazii se meze nekradou. Říká se to takhle, ne ?

Includováná v javascriptu:

<script src="https://iiic.dev/js/modules/importWithIntegrity.mjs" integrity="sha256-bad-integrity" crossorigin="anonymous"></script>
<script src="https://iiic.dev/js/modules/importWithIntegrity.mjs" crossorigin="anonymous"></script>

První řádek selže (kvůli špatnému subresource integrity integrity="sha256-bad-integrity"), ale 2. už projde (žádná kontrola integrity). Běžný javascript je možné načítat opakovaně.

Jiná situace je ale u modulů:

<script type="module" src="https://iiic.dev/js/modules/importWithIntegrity.mjs" integrity="sha256-bad-integrity" crossorigin="anonymous"></script>

(povšimněte si type="module")

všechny myslitelné pokusy o vložení stejného souboru poté selžou:

<script type="module" src="https://iiic.dev/js/modules/importWithIntegrity.mjs" crossorigin="anonymous"></script>

<script src="https://iiic.dev/js/modules/importWithIntegrity.mjs" crossorigin="anonymous"></script>

ať už v head či v body… pro tento účel je chování shodné:

<script>
	const element = document.createElement( 'SCRIPT' );
	element.src = "https://iiic.dev/js/modules/importWithIntegrity.mjs";
	document.body.appendChild( element );
</script>

<script type="module">
	const element = document.createElement( 'SCRIPT' );
	element.src = "https://iiic.dev/js/modules/importWithIntegrity.mjs";
	document.body.appendChild( element );
</script>

<script>
	import { importWithIntegrity } from 'https://iiic.dev/js/modules/importWithIntegrity.mjs';
</script>

<script type="module">
	import { importWithIntegrity } from 'https://iiic.dev/js/modules/importWithIntegrity.mjs';
</script>

… a další možnosti vložení. Žádná neprojde (Failed to find a valid digest in the 'integrity' attribute for resource 'https://iiic.dev/js/modules/importWithIntegrity.mjs' with computed SHA-256 integrity 'cfOxrPKw+HRch/o1HAGTjzeo5g+8Ho2VW5Ki75Y6DII='. The resource has been blocked.). Důvodem je speciální chování javascriptových modulů, vkládají se pouze jednou . A to i v případě, že pokus o vložení napoprvé selže z důvodu špatné subresource integrity.

Podobně by mohl sloužit i preload:

<link rel="preload" as="script" href="https://iiic.dev/js/modules/importWithIntegrity.mjs" integrity="sha256-some-integrity-string" crossorigin="anonymous">

ale narazíte na zásadní rozdíl mezi prohlížeči Chrome (testováno na verzi 79) a Firefox (testováno na verzi 71), kde v Chrome se integrita u preloadu kontroluje a pokud nesedí, tak se zablokuje další načítání souboru, ovšem ve Firefoxu ne, tam se při preloadu integrita nekontroluje a další načtení se provede. Z tohoto důvodu je link rel="preload" k tomuto účelu nepoužitelný.

K preloadu modulů by měl sloužit nový link rel="modulepreload" ovšem podpora je zatím špatná a pro potřebu kontorly integrity ve Firefoxu selhává zcela stejně jako link rel="preload" . Prostě horká novinka, zatím až moc horká.

Nejsou 2 načítání toho samého javascriptu problém?

Ne, totiž, jak kdy :) . V případě modulů to problém není, ty se načtou pouze 1x. V případě že moduly nepoužíváte to problém je. Pak by se zbytečně jak přenášely data (pokud by tam nebyl nějaký agresivnější druh cacheování) tak prováděl obsah javascriptu.

A ostatní prohlížeče?

Opera (verze 64) se chová stejně jako Chrome, tedy plná podpora. Firefox neumí preload, jak bylo popsáno výše. IE nemá podporu vůbec a Edge (zatím) také ne. Viz sekce tohoto článku podpora modulů.

Co z toho tedy plyne?

Že javascriptové moduly jsou super, používejte javascriptové moduly:

  1. Automaticky silná cache (to samé lze dosáhnout i bez modulů, pomocí Cache-Control: immutable, ale tady máte toto chování ve výchozím stavu)

  2. Module se načítá a provádí pouze 1x opakované includy nejsou problém. Né že by byli problém i bez modulů, to není nic, co by vývojáře trápilo, ale když používáte dynamický import dostáváte se ke zcela novým možnostem, kód:

    someHugeArray.forEach(( item ) => {
    	import( 'myGreatModule.mjs' ).then( ( /** @type { Module } */ module ) =>
    	{
    		doSomethingWith( module );
    	} );
    });
    

není problémem, sice je import uvnitř cyklu, ale ke stahování i provádění souboru myGreatModule.mjs dojde pouze při prvním průběhu cyklu.

  1. Izolace… moduly jsou o něco více izolované než běžné scripty (ne až tak drasticky, jako service workery, naštěstí, ale to by bylo povídání na samostatný článek), ale ukázat si to můžeme klasicky na příkladu:

    
    <script>
     	console.log(this); // objekt Window
    </script>
    <script type="module">
     	console.log(this); // 'undefined'
    </script>
    

Nemusíte se ale bát, že se nedostanete k Window případně window.document a dalším. Až takhle striktní izolace není, jen musíte přistoupit přímo přes objekt (window), nemůžete použít lokální kontext this.

Díky izolaci se také kód z modulu vykoná o něco rychleji než javascript bez modulu, ale výkon tím nespasíte (zrychlení je minimální), spíše jen taková zajímavost k dobru modulů.

Dobře, ale jaká je podpora modulů?

Obsáhle vyjádřeno: různá, totiž prohlížeče implementují tuto technologii po částech. Proto například když vkládáte 1 modul uvnitř jiného javascriptu ( třeba uvnitř modulu ), má to jinou podporu, než když vkládáte modul do HTML stránky . Ve zkratce by se dalo najít úzké hrdlo, kterým je nulová (a tohle už se hádám nezlepší) podpora v IE a stejně tak nedostatečná (neumí ty dynamické importy) podpora v Edge, tam se to naštěstí zlepší, už teď funguje v nové Edge betě založené Edge na jádru Chromia.

Zpátky k tématu

No a protože šetříme čase, co takhle si vytvořit funkci pro import s kontrolou integrity?

A neudělal to už někdo, někdo jiný? No nevypadá to, Google by mi to jinak řekl. Respektive je na to knihovna RequireJS, ale není řešení v podobě 1 funkce bez nutnosti přidávat celou knihovnu (2.2 MiB v zipu). RequireJS toho umí spoustu, jenže né všechno z toho potřebuji, prakticky nic. Bylo by to krásné, kdyby knihovny byli rozdělené do modulů a člověk mohl použít jen to, co potřebuje. Snad v budoucnu, já jsem optimista. A jsem optimista i ohledně tématu… import s integritou se připravuje, zatím ve stádiu návrhu. K použití v prohlížeči je ale dlouhá cesta, napřed se zohlední komentáře a připomínky, pak se návrh schválí a pak se objeví v prohlížečích a pak po čase až tyto verze prohlížečů budou mít na svých strojích uživatelé můžeme používat. A samozřejmě nesmíme zapomínat na nadšence v teamech vývojářů prohlížečů. Třeba Chrome už několikrát ukázal, že věc kterou sám chce udělat standardem napřed nasadí do prohlížeče a až pak si projde to legislativní kolečko tvorby standardu.

Takže vlastními silami…

Způsobů jak zapsat import je hodně, krásně to popsali v MDN:

import defaultExport from "module-name";
import * as name from "module-name";
import { export1 } from "module-name";
import { export1 as alias1 } from "module-name";
import { export1 , export2 } from "module-name";
import { foo , bar } from "module-name/path/to/specific/un-exported/file";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export1 [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";
var promise = import("module-name");

zdroj: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Syntax

Mohl by pomoct preload například:

<link rel="preload" as="script" href="https://iiic.dev/js/modules/importWithIntegrity.mjs" integrity="sha256-cfOxrPKw+HRch/o1HAGTjzeo5g+8Ho2VW5Ki75Y6DII=" crossorigin="anonymous">

Jenže jak jsem popsal výše situace s Firefoxem je nešťastná a proto musíte použít prostě javascript:

<script type="module" src="https://iiic.dev/js/modules/importWithIntegrity.mjs" integrity="sha256-cfOxrPKw+HRch/o1HAGTjzeo5g+8Ho2VW5Ki75Y6DII=" crossorigin="anonymous"></script>

následně libovolný způsob importu, to jestli se provede nebo ne bude záležet právě na odpovídající hodnotě atributu integrity.

Dynamické importy

Jistě by někdo mohl říct že mezi preloadem a kompletním načtením (a provedením) javascriptu je velký rozdíl. Moduly s tím počítají a zavádí dynamický import. Jak ale zajistit ten proti změně zdrojového souboru kontrolou integrity?

Co takhle upravit funkci import()

Ale javascript je úžasně tvárný (někdo kdo z možnosti upravit si libovolnou funkci není tak nadšený by možná použil jiné slůvko než úžasně, ale to je jiný příběh). Takže co upravit standardní funkci import() tak, aby vyžadovala subresource integrity string? To je opravdu špatný nápad, ale někdo si tu slepou uličku musel projít, a říct "tudy ne, přátelé". Totiž už to že import() je funkce je samo o sobě chyba. Takže vlastně celý nadpis tohoto odstavce je tak trochu podvod. Vypadá to jako funkce, ale není. V MDN to popsali slovem function-like to je myslím docela přesné. Tahle specialita obnáší mimo jiné to, že ji nelze přetížit. A když vytvoříte funkci pojmenovanou import() dostanete se do kolizního stavu, který nemá jednoznačné řešení. Prohlížeče si s ní ale poradí tím způsobem že použijí buď vaši novou funkci nebo zmíněné function-like chování podle kontextu. Nicméně nativní function-likeimport() prostě přetížit nelze. Ono stejně v mnoha příručkách o javascriptových best practice se dočtete že přetěžování nativních funkcí není vhodné. Jinde jsou zase případy kdy to vhodné je. Tak jak tak v tomto případě nemáte na výběr, takže to nyní rozhodně neotvírejme jako otázku.

Nezbývá než tedy vytvořit si funkci novou, která může sama provést víše popsaný postup spočívající ve vytvoření preloadu v hlavičce stránky se subresource integrity kontrolou a následně provede dynamický import javascriptem.

Může vypadat například takto:

export function importWithIntegrity ( /** @type { String } */ path, /** @type { String } */ integrity )
{
	const POSSIBLE_HASHES = [ 'sha256', 'sha384', 'sha512' ]; // same length… 6 chars
	const INTEGRITY_DIVIDER = '-';

	if ( !integrity ) {
		integrity = 'is missing!';
	}
	if (
		!POSSIBLE_HASHES.includes( integrity.substring( 0, 6 ).toLowerCase() )
		|| integrity.substring( 6, 7 ) !== INTEGRITY_DIVIDER
	) {
		integrity = POSSIBLE_HASHES[ 0 ] + INTEGRITY_DIVIDER + integrity;
	}

	/** @type { HTMLScriptElement } */
	const element = ( document.createElement( 'SCRIPT' ) ); // link rel="preload" also working, but NOT in Firefox :(

	element.type = 'module';
	element.src = path;
	element.integrity = integrity;
	element.setAttribute( 'crossorigin', 'anonymous' );
	document.head.appendChild( element );
	return new Promise( ( /** @type { Function } */ resolve ) =>
	{
		import( path ).then( ( /** @type { Module } */ module ) =>
		{
			resolve( module );
		} );
	} );
}

( https://iiic.dev/js/modules/importWithIntegrity.mjs )

Pro použití nyní máte 2 možnosti, buď tuto funkci includovat do globálního scope, nebo ji volat až v případě že je skutečně potřeba. To vám sice může způsobit mírné zpomalení scriptu (pokud ještě není modul nacacheovaný v prohlížeči uživatele), ale zase ušetřit nějaká data v případě že do tohoto stavu se javascript dostane jen příležitostně. To už je na zvážení každého programátora a konkrétní situaci:

importWithIntegrity(
	'https://iiic.dev/js/modules/url/completeHash.mjs',
	'sha256-csBcPTKCf0Z5pJMrAtLx2B4mx25eSqDSgtoAxbVKu0I='
).then( ( /** @type { Module } */ completeHash ) => {
	new completeHash.append( URL );
	return new URL(window.location.href).completeHash();
} ).then( ( str ) => {
	console.log( str );
} );

Samotná nová funkce importWithIntegrity() umožní načíst libovolný modul a přitom zkontroluje jeho subresource integrity, ale neumí takto načíst sama sebe, takže nezapomeňte na kontrolní součet při vkládání scriptu v hlavičce stránky (nebo kdekoliv jinde, tak jak tak se modul chová jako defer).

ic