Jak princezna finálně zatočila s (DOM) XSS
Podívejte se raději na online verzi přednášky, slajdy mohly být aktualizovány nebo doplněny.
Detail přednášky
There's an English version of the talk here.
Ta princezna se jmenovala Mozilla a její princ byl Chrome(j). A tihle dva se rozhodli, že už bylo dost všech těch bugů a vyplacených odměn za nahlášení Cross-Site Scriptingu a tak upekli DOMa-based ochranu přímo ve svých výtvorech.
Princezna s princem vám ústy svého tiskového mluvčího a dvorního šaška v jedné osobě poví co je to ten XSS, že klientskou variantu nezastaví escapování na serveru, a že sanitizér není to mejdlo na ruce. A taky o tom, jak pomohou Trusted Types, a že to všechno není jen pohádka, ale už (skoro) realita.
Přednáška a slajdy jsou včetně ukázek, které si můžete sami vyzkoušet.
Datum a akce
17. února 2022, JSDays 2022 (délka přednášky 60 minut, 19 slajdů)
Slajdy
#1 DOM (Document Object Model) XSS (Cross-Site Scripting) je typ XSS útoků, ke kterým dochází v JavaScriptových aplikacích v browserech bez nutnosti doručení zákeřného JavaScriptu v odpovědi ze serveru. Princezna Mozilla a hlavně princ Chrome(j) se rozhodli s tím něco udělat. Tohle je přednáška o blízké budoucnosti.
#2 Tohle jsou tři typy XSS:
- Stored XSS (někdy též „permanent XSS“)
- Reflected (též „temporary“)
- DOM XSS (někdy „DOM-based XSS“).
Následuje malé opakování těchto variant, ale nepůjdeme moc do hloubky.
#3 Při Stored variantě útočník (ninja kočka vlevo) uloží (krok 1) nebo zastoruje zákeřný JavaScript do nějakého úložiště, typicky databáze, úpravou např. poznámky při objednávce, doručovací adresy apod. a pak aplikace doručí tento JavaScript do browseru (krok 2) každému uživateli, který si prohlédne stránku, která tento zákeřný JavaScript vypíše do browseru bez správného ošetření. JavaScript pak v browseru může ukrást cookie, zobrazit falešný login formulář, posílat HTTP požadavky a skenovat vnitřní síť z browseru běžícího uvnitř této sítě. Co všechno takový JavaScript umí se dozvíte třeba v mé přednášce o XSS, ukázku pak najdete ve videu od 14. minuty.
#4 Když mám za úkol něco pojmenovat a já nevím, tak píšu alerty. A já nevím dost často, teď třeba zrovna nevím jak pojmenovat budoucího potomka (dík za optání a jo, „Alert Špaček“ jsem už navrhnout zkusil). Píšu to skoro všude, většinou jen tak, z takové velmi specifické legrace.
#5 Někdy se opravdu zasměju, jindy jen pousměju. Třeba když Bitbucket tenhle můj název SSH klíče pro použití s Gitem vypsal do stránky přesně tak jak jsem ho nazval. Můj browser poté ten JavaScript spustil a zobrazil se alert. Přišel jsem i na to jak by se tohle dalo zneužít i proti někomu jinému než proti mě samotnému (takovým „proti mě samotnému“ útokům se říká Self-XSS, velmi často probíhají stylem „otevřete developer tools a do konzole vložte tento kód a budete mít starý Facebook (a já budu mít třeba vaše cookies)“) a výrobce Bitbucketu mě za to zařadil na svou zeď slávy za 2014. Na mém webu je JavaScript třeba i v HTTP hlavičkách i DNS záznamech, protože proč ne, všechno je uživatelský vstup – v některých případech i to může způsobit problém, ale nebojte, můj JS jen zobrazí nějaké ty gify a videa.
#6 Obrana proti Stored XSS spočívá ve správném ošetření nebezpečných znaků na serveru pomocí funkcí jako je htmlspecialchars()
např. (převádí znaky <
>
"
'
na entity, čímž zruší jejich speciální význam pro HTML) nebo ještě lépe pomocí nějakých šablonovacích systémů, které to escapování (převod na entity) dělají automaticky a vždy – takže na to nemůžete zapomenout, když máte deadline nebo hangover. Escapování se dá považovat za primární úroveň zabezpečení proti XSS, sekundární pak může být třeba Content Security Policy (CSP). Pomocí CSP můžete browseru říci, odkud může stahovat obrázky a fonty, které skripty může spouštět a kam může posílat formulářová data, takže pokud by útočník do stránky dostal nějaký zákeřný JavaScript přece jen dostal, tak ho browser nejspíš nespustí. CSP ale není primární úroveň, jen doplňková. CSP dnes nebudeme detailně probírat, jen se o to trochu, spíš tak nějak mimochodem, otřeme později. Pro další info koukněte na tuhle přednášku o CSP a tu o CSP Level 3.
#7 Při Reflected XSS variantě útočník nejdříve přiměje uživatele kliknout na odkaz, ve kterém je obsažen nějaký zákeřný JavaScript (krok 1). Kliknutím se ten zákeřný JS přenese až do aplikace (krok 2), která ho vypíše zpět do prohlížeče uživatele, který na odkaz kliknul (krok 3). Dochází k jakémusi odrazu zákeřného kód od aplikace zpět do prohlížeče, proto název „reflected“. Představte si to třeba na stránce s výsledky vyhledávání na adrese /search?query=<script>…</script>
, která vypíše např. „Výsledky vyhledávání $query“.
#8 Ochrana proti Reflected XSS je v podstatě stejná jako u Stored XSS: převod nebezpečných znaků na entity na serveru pomocí funkcí jako htmlspecialchars()
nebo lépe pomocí šablonovacích systémů a Content Security Policy jako další úroveň zabezpečení, kdyby ta primární selhala, viz dříve. V prohlížečích existovala (a v Safari ještě stále existuje od Safari 15.4 už ani tam a naopak ve Firefoxu neexistovala nikdy) ochrana pomocí XSS Auditoru (někdy „XSS Filter“). Ten sledoval, jestli v odchozím požadavku (krok 2 na předchozím slajdu) není něco co vypadá jako JavaScript, a pokud se to vrátilo i zpět ze serveru (v kroku 3), tak to XSS Auditor prohlásil za Reflected XSS a dle nastavení nebo verze prohlížeče stránku buď „opravil“ a zobrazil ji bez toho zákeřného JS, nebo načítání úplně zablokoval a zobrazil celostránkové varování. To zní jako Dobrej Nápad™, akorát že vůbec. Navíc XSS Auditor někteří vývojáři brali jako obecnou ochranu proti XSS, protože „jaká security chyba, hele, mě ten alert browser nezobrazí, žádná chyba tu není“. Navíc XSS Auditor nedokázal vždy poznat, že v požadavku je JS a bylo běžné jeho obcházení. CSP je lepší nástroj a tak to Auditor měl spočítáno. Nejdříve ho odstranil původní Edge a časem i Chrome a prohlížeče postavené na Chromium (tedy i Chromedge).
#9 XSS Auditor umožňoval získávat nebo prohledávat stránky, které uživateli taková zranitelná aplikace posílala. Proto název XS-Leaks nebo XS-Search, kde XS znamená „Cross-Site“. Představte si, že aplikace uživateli pošle nějaký JS kód (např. var signedin = true
) a útočník chce zjistit, jestli je uživatel do takové zranitelné aplikace přihlášen. Pošle uživateli odkaz (1) např. https://example.com/neco?<script>var signedin = true</script>
, XSS Auditor detekuje Reflected XSS (2, 3), načtení stránky zablokuje a browser tak nemůže načíst např. obrázek, který tam útočník také vložil. Útočník také může pomocí vkládaných framů zjistit jak dlouho se stránka načítala a z toho vyvodit přítomnost zablokovaného JS apod. (4).
#10 Jenom za hlášení XSS chyb Google za roky 2015–2016 vyplatil 1,2 milionu USD. Mimo jiné to signalizuje, že XSS útoky jsou celkem schopná věc a mohou být celkem zdrcující.
#11 Jen pro představu, tohle je prý milion dolarů v jednodolarových bankovkách v muzeu v Chicagu (zdroj).
#12 Google neuvádí přesně za co ten milion vyplatil, ačkoliv by se to možná dalo někde postupně dohledat. Nemálo peněz vyplácí i za tu třetí variantu: DOM-based XSS. Tady za tuhle chybu vyplatil hezky kulatou sumu 3133,7 USD. Pro Stored a Reflected XSS už navrhli Content Security Policy jako další úroveň zabezpečení, takže už bylo na čase něco udělat i s DOM XSS.
#13 Na Sanitizer API spolupracuje Google, Mozilla a Cure53. V současnou chvíli (únor 2022) je v browserech zatím defaultně vypnutý (v Chrome je část API dostupná veřejně od verze 105), protože není ještě zcela dokončena implementace, a podporu je potřeba zapnout v chrome://flags/#enable-experimental-web-platform-features
nebo about://config (dom.security.sanitizer.enabled
). Sanitizer API je také dostupný pouze pro stránky na HTTPS, přesněji jen pro „secure contexty“. Sanitizer API slouží pro „vyčištění“ HTML, pokud ho chcete v JavaScriptu někam přiřadit nebo zobrazit v nějakém elementu na stránce, aby tam právě někdo nepropašoval nějaký zákeřný JS. Na takové čištění se často používá třeba knihovna DOMPurify shodou okolností (nebo spíš ne) také od Cure53.
#14 Sanitizer nabízí metody pro čištění HTML a prvkům přidává metodu setHTML()
. Vyzkoušet si to můžete v podstatě na jakékoliv doméně, ale pojďme na https://example.com/?foo=FOO%3Cimg%20src=x%20onerror=alert(1)%3EBAR.
Parametr foo
není vypisován do stránky, ale použijeme ho pro simulaci DOM XSS. Načtěte ten link a do developer tools konzole napište
const el = document.getElementsByTagName('h1')[0]
const param = new URLSearchParams(location.search).get('foo')
el.innerHTML = param
Tím obsah značky H1
nahradíme za obsah parametru foo
a neošetřeným zápisem do innerHTML
způsobíme DOM-based XSS. Poté to budeme chtít sanitizérovat:
const s = new Sanitizer()
el.setHTML(param, s)
Sanitizer odstranil nebezpečný tag onerror
, ale značku IMG
ponechal. Stejného výsledku bychom dosáhli zápisem očištěného řetězce do innerHTML
takto:
el.innerHTML = s.sanitizeFor('h1', param).innerHTML
Pro odstranění všech HTML značek použijte jinak nakonfigurovaný Sanitizer (nefunguje se setHTML(…, s)
, jen s sanitizeFor()
, prozatím?):
const s = new Sanitizer({allowElements:[]})
A pak už stačí jen projít kód a všechny nebezpečný věci nahradit Sanitizerem 😅
#15 DOM-based XSS vzniká, když se do tzv. „sinků“ zapíše neošetřený vstup. Jedním z takových sinků je vlastnost innerHTML
, dalším je např. funkce eval()
, obě dokáží spustit libovolný JavaScript (mezi značkami SCRIPT
a v onerror
atributech apod.) Trusted Types mohou zajistit, aby se takový libovolný JS nespustil.
#16 Trusted Types (podporuje zatím jen Chrome, podporu není třeba nijak zapínat, pro jiné browsery existuje tzv. polyfill) umí zakázat zápis libovolných řetězců do sinků a nahradit ho zápisem objektů třídy TrustedHTML
. Trusted Types nám pomohou dokázat, že používáme bezpečný zápis, protože ten nebezpečný ani nepůjde použít. Objekty TrustedHTML
můžeme vytvářet buď ručně, nebo to celé můžeme nechat na automagice. Trusted Types se dají použít i jen k nalezení sinků, zkuste si to na mém demo webu, všimněte si CSP hlavičky, která vyžadování Trusted Types zapíná (a její „report-only“ varianty, která zajistí, že se stránka bude normálně načítat, ale budou se jen posílat reporty) a direktivy require-trusted-types-for 'script'
. Rozklikněte si kód a uvidíte i zápis do sinku innerHTML
. Pokud tam tlačítkem Enter any HTML něco zapíšete, browser to sice udělá, ale postěžuje si do konzole a pošle report, který uvidíte pod odkazem Reports.
#17 V dalším kroku zkusíme už TrustedHTML
objekt vyžadovat, jinak zápis nedovolíme, to si zkuste na další demo stránce – všiměte si hlavičky CSP bez report-only
a vytváření politiky pomocí trustedTypes.createPolicy
a její následné použití pro zápis do innerHTML
.
Escapování nechávají Trusted Types na vás, můžete použít DOMPurify, Sanitizer nebo obyčejný string.replaceAll()
. Jak dobré escapování, tak dobré Trusted Types. Když se pokusíte do sinku zapsat string (tlačítko Enter any HTML to také zkusí), tak se to nepovede a pošle se report. Pokud by se vám nechtělo vytvářet objekty TrustedHTML
ručně, tak můžete vytvořit escapovací politiku s názvem default: trustedTypes.createPolicy('default', …)
a ta se pak použije vždy při zápisu do sinků, v tomto případě ale escapujte fakt dost solidně. Používání defaultní policy je k vidění na další stránce.
Tahle automatizace a prokazatelnost se mi moc líbí a těším se, až to bude ještě víc použitelný. V současný době spousta knihoven do sinků zapisuje, takže Trusted Types se často nedají provozovat ani v tom „report-only“ režimu. Možná jste si všimli, že JavaScript na těch mých Trusted Type demo stránkách není obarven, už je, knihovnu highlight.js jsem nahradil naivním obarvováním řetězců na serveru. Highlight.js totiž zapisuje do innerHTML
a i jen při načtení té stránky to vygenerovalo dva reporty. Ale snad to vývojáři nějak brzo vyřeší.
#18 Tohle je možná trochu nečekaný sink. Když používáte Trusted Types a vytvoříte element SCRIPT
, tak vlastnost src
nepřijme obyčejný string a místo toho budete muset přiřadit objekt třídy TrustedScriptURL
. To je proto, že obyčejný string může pocházet z uživatelského vstupu, což může skončit tak, že browser nahraje JS z nějakého útočníkovo serveru.
Ukážeme si to na stránce, kterou jsme už používali pro ukázku Trusted Types. Jděte na ni a otevřete konzoli developer tools a spusťte:
const script = document.createElement('script');
Když teď zkusíte nahrát nějaký externí soubor, např. takto:
script.src = 'http://example.com/foo.js';
tak dostanete chybovou hlášku, která říká, že je potřeba TrustedScriptURL
: „This document requires ‚TrustedScriptURL‘ assignment“. Pošle se i report, koukněte na něj kliknutím na odkaz „Reports“. Ten objekt se ale stejně vytvoří, což si můžete ověřit napsáním script
, tedy jména té proměnné a zmáčknutím Enteru. Vytvořil se proto, že stránka používá hlavičku Content-Security-Policy-Report-Only
, přičemž důležitá je ta koncovka „Report-Only“.
Pojďme teď zkusit vytvořit nějakou policy, třeba takhle:
const policy = trustedTypes.createPolicy('LePolicy', {
createHTML: (string) => string.replace(/>/g, "<"),
createScriptURL: (string) => string.replace('http://', 'https://'),
});
Využili jsme již existující kód a jen jsme přidali jednu novou metodu createScriptURL
. Pro jednoduchost ta metoda jen změní protokol z http://
na https://
, v opravdových aplikacích by jste asi kontrolovali nebo nějak přepisovali doménu nebo tak něco. I tady platí, že je to komplet na vás, podobně jako to co děláte v metodě createHTML
.
Teď tu metodu zavolejte, čímž se vytvoří ten objekt TrustedScriptURL
, a výsledek přiřaďte do vlastnosti src
:
script.src = policy.createScriptURL('http://example.com/foo.js');
Změnu protokolu si opět ověřte pomocí napsání script
a stisknutí Enteru.
I v tomhle případě můžete použít defaultní politiku, reloadněte tu stránku a spusťte tohle:
const script = document.createElement('script');
trustedTypes.createPolicy('default', {
createHTML: (string) => string.replace(/>/g, "<"),
createScriptURL: (string) => string.replace('http://', 'https://'),
});
script.src = 'http://example.com/foo.js';
A teď už vážně naposled, ověřte si výsledek napsáním script
a stisknutím Enteru.
#19 Během posledních cca 10 let se objevilo a stále objevuje docela dost desktopových aplikací, které ale pod kapotou používají klasické webové technologie jako HTML a JavaScript. Což je na jednu stranu paráda, ale na druhou stranu to přináší i některé moderní problémy. Protože najednou útoky jako např. XSS, což je klasický webový útok, mohou být použity proti desktopovým uživatelům. Cross-platform, ftw!
A jak předvedl Microsoft, tak modern problems require modern solutions, že. No a tak jejich nový Teams jsou na desktopu v podstatě webovou stránkou s jakýmsi obalem, což jim dovolilo použít Trusted Types a to mi přijde dost dobrý: prostě použili webovou obranu proti webovému útoku. V desktopový aplikaci. What a time to be alive!