Check the online version, I often update my slides.

Talk detail

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.

Date and event

February 17, 2022, JSDays 2022 (talk duration 60 minutes, 17 slides)

Slides

Jak princezna zatočila s (DOM) XSS

#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.

Opáčkross-site scripting

#2 Tohle jsou tři typy XSS:

  1. Stored XSS (někdy též “permanent XSS”)
  2. Reflected (též “temporary”)
  3. DOM XSS (někdy “DOM-based XSS”).

Následuje malé opakování těchto variant, ale nepůjdeme moc do hloubky.

Stored XSS: 🐱‍👤 ➡1️⃣➡ 🖥 ➡2️⃣➡ 👩‍💻

#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.

Edit SSH key, label: <script>alert("XSS');</script>

#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.

The page at https://bitbucket.org says: XSS

#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.

htmlspecialchars() & Content Security Policy

#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.

Reflected XSS: 🐱‍👤 ➡1️⃣➡ 👩‍💻 ➡2️⃣➡ 🖥 ➡3️⃣➡ 👩‍💻

#7 Při Reflected XSS variantě útočník nejdříve přiměje uživatele klinout 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”.

Stored XSS: htmlspecialchars() & CSP (& XSS Auditor)

#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).

Google Bug Bounty XSS: $1.2M in 2015-2016

#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í.

Million Dollar Cube v muzeu v Chicagu

#11 Jen pro představu, tohle je prý milion dolarů v jednodolarových bankovkách v muzeu v Chicagu (zdroj).

DOM-based XSS bounty

#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.

Sanitizer API

#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 nebo about://config. 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.

const s = new Sanitizer(); el.setHTML(text, s); el.innerHTML = s.sanitizeFor('h1', text).innerHTML

#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štř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 😅

DOM-based XSS sink: el.innerHTML

#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.

🙅‍♂️ el.innerHTML = '<img src=a.jpg>' 👉 el.innerHTML = <TrustedHTML>

#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.

🙅🏽‍♂️ el.innerHTML = '<img src=a.jpg>' 👉🏽 el.innerHTML = <TrustedHTML>

#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ší.

Michal Špaček

Michal Špaček

I build web applications and I'm into web application security. I like to speak about secure development. My mission is to teach web developers how to build secure and fast web applications and why.

Public trainings

Come to my public trainings, everybody's welcome:

HTTPS for developers & admins
(December 7–8, 2022 )

PHP application security
(December 12–15, 2022 )