V júni sme Penetrify pustili na našu vlastnú aplikáciu, ako to robíme pred väčším releasom. Scan sa vrátil s problémom v našom prihlasovaní cez Cognito, a keď som za tú nitku zatiahol, vyšlo z toho niečo, čo som nečítal rád: dashboard Penetrify držal access a refresh tokeny z Cognita v localStorage, v plaintexte, čitateľné akýmkoľvek JavaScriptom na stránke.
Nesadli sme si a takto sme sa nerozhodli. Je to default auth knižnice, ktorú sme použili, a nikdy sme ho nespochybnili. Práve to je na tom to zaujímavé.
Čo presne bol ten nález
Náš dashboard je single-page aplikácia. Po prihlásení dostala tokeny z AWS Cognita a SDK ich uložilo do browser storage, pretože tam ich SDK ukladajú, keď v session nie je zapojený server. Prečítať ich mohol akýkoľvek skript s prístupom na stránku.
Znamená to, že jedna cross-site scripting chyba kdekoľvek v aplikácii, jedno škodlivé rozšírenie prehliadača alebo jedna kompromitovaná npm závislosť, a útočník odchádza s oboma tokenmi. Access token ho dostane do účtu. Refresh token ho tam udrží, pretože zostáva platný dlho po tom, ako session vyzerá zatvorená, kým ho niečo výslovne nezneplatní.
U našich užívateľov boli v dosahu fakturačné údaje, výsledky scanov ich aplikácií a dáta o affiliate výplatách. Nakoľko to vieme posúdiť, nikto to nezneužil. O to však nejde. Bolo to dosiahnuteľné a bol to náš vlastný default na opravu.
Prečo šifrovanie localStorage nie je riešenie
Prvý instinkt je tokeny pred uložením zašifrovať. Nepomôže to. Dešifrovací kľúč musí žiť v tom istom JavaScripte, ktorý útočník už spúšťa. Pridal si krok, nie hranicu.
Druhý instinkt je presunúť tokeny do cookie na strane klienta. To tiež nepomôže. Cookie nastavená JavaScriptom je JavaScriptom čitateľná. Vlastnosť, na ktorej záleží, je HttpOnly, a nastaviť ju dokáže len server, hlavičkou Set-Cookie. Ak tvoja oprava beží v prehliadači, nie je to oprava.
Čo sme nasadili namiesto toho
Celú session sme presunuli do cookies nastavovaných serverom, vzorom token handler vnútri backendu, ktorý sme už mali:
- FastAPI backend vykonáva volania na Cognito a vracia session ako cookies s
HttpOnly,SecureaSameSite. Prehliadač token nikdy nevidí. - Auth middleware číta access token z cookie namiesto hlavičky
Authorization. - Pretože sa cookies posielajú automaticky, mutujúce requesty teraz nesú CSRF token v hlavičke, ktorý backend overuje.
- Odhlásenie vykoná global sign-out na strane Cognita, takže token, ktorý niekedy unikol, umrie so session namiesto toho, aby ju prežil.
- Auth SDK je z prehliadača úplne von.
Nepotrebovali sme na to novú službu. Naša statická aplikácia a naše API sú už teraz na tom istom origine: CloudFront servíruje aplikáciu a route /api/* posiela na API Gateway. First-party cookie sa tak posiela s každým volaním API bez akejkoľvek ďalšej infrastruktúry.
Najprv sme sa pozerali na cestu odporúčanú vendorom, čo by znamenalo prevziať hostované prihlasovacie UI, a zamietli sme ju. Zahodila by naše vlastné obrazovky pre login, MFA a reset hesla kvôli problému, ktorý dokážeme vyriešiť v existujúcom backende.
Čo to stálo
Asi týždeň práce a žiadnu viditeľnú zmenu pre užívateľov. To je úprimný súhrn väčšiny bezpečnostnej práce a presne preto sa takéto veci v startupe odkladajú do nekonečna. Na roadmape sa nič nepohne. Žiadny zákazník o to nepožiada. Zmení sa len to, čo s tvojimi užívateľmi dokáže urobiť jediný injektovaný skript.
Jeden praktický detail, ktorý nasadenie zbezpečnil: počas rolloutu backend stále prijímal starú cestu cez hlavičku, takže revert frontend deployu bol rollback na jeden krok, keby sa čokoľvek pokazilo.
Over si vlastnú aplikáciu. Zaberie to asi 30 sekúnd.
Otvor svoju aplikáciu, prihlás sa a otvor DevTools:
- Záložka Application, Local Storage. Hľadaj dlhé hodnoty s dvoma bodkami. Tento tvar je JWT.
- V tom istom paneli skontroluj Session Storage. Rovnaký problém, kratšia životnosť.
- V konzole napíš
document.cookie. Čokoľvek sa tam objaví, je z definície čitateľné JavaScriptom, takže session cookie, ktorú v tom výpise vidíš, ťa nechráni.
Ak token nájdeš, je to nález. Nie teoretický, a nie taký, na ktorý by ťa upozornil zelený audit závislostí alebo čistý beh statickej analýzy.
Prečo to publikujeme
Dva dôvody, a ten druhý je o našom vlastnom produkte.
Prvý: defaulty sú rozhodnutia, ktoré za teba urobil niekto iný. Prídu s knižnicou, sú pohodlné a takmer nikdy nie sú threat model. Ten náš bol rozumný default pre tutoriál a zlý pre aplikáciu, ktorá drží platobné dáta.
Druhý: scan je ukazovateľ, nie verdikt. Ten náš označil prihlasovacie flow a dokázal, že je dosiahnuteľné. Nepodal nám vetu „tvoja auth knižnica má default localStorage a práve to treba zmeniť". Cesta od nálezu k oprave znamenala otvoriť si vlastnú auth cestu a prečítať ju. Presne tak očakávame, že budeš používať aj naše reporty: nález ti povie, kam sa pozrieť, a ukáže request, ktorý to dokazuje, a ty sa potom rozhodneš, čo je správna oprava v tvojej architektúre.
Takže: pusť scan na vlastnú aplikáciu a potom si prečítaj kód, na ktorý ukazuje. Urobili sme oboje a ani jedna polovica by sama nestačila.
Ak chceš pohľad na svoju aplikáciu zvonku hneď teraz, náš bezplatný security check zaberie asi 60 sekúnd a nechce registráciu. Ak chceš plný report s dôkazmi a opravami, prvý scan je za $29 a započíta sa do plánu. A ak chceš radšej tú vec zadarmo, ktorú sme opísali vyššie, otvor DevTools a pozri sa do svojho local storage. Tá nestojí nič.
