CSRF atakos: kaip apsaugoti web aplikacijas

Kas yra CSRF ir kodėl tai turėtų jus neraminti

Įsivaizduokite situaciją: sėdite kavinėje, naršote internete ir, nieko neįtardami, paspaudžiate ant nekaltai atrodančios nuorodos. Per kelias sekundes iš jūsų banko sąskaitos išsiunčiamas pavedimas, pakeičiami el. pašto nustatymai arba jūsų vardu paskelbtas įrašas socialiniame tinkle. Skamba kaip scenarijus iš kibernetinio trilerio? Deja, tai realybė, su kuria susiduria daugelis web aplikacijų – ir vadinama Cross-Site Request Forgery arba CSRF.

CSRF ataka išnaudoja tai, kad jūsų naršyklė automatiškai siunčia autentifikacijos duomenis (dažniausiai slapukus) kiekvieną kartą, kai kreipiamasi į tam tikrą svetainę. Užpuolikas sukuria specialią užklausą, kuri atrodo teisėta serverio požiūriu, nes ateina su galiojančiais jūsų slapukais. Serveris negali atskirti, ar tai tikrai jūs paspaudėte „Pervesti pinigus” mygtuką, ar tai padarė kenkėjiška programa jūsų vardu.

Problema ta, kad daugelis kūrėjų vis dar mano, jog paprastos autentifikacijos pakanka. Spoiler alert: nepakanka. CSRF atakos veikia net tada, kai esate visiškai autentifikuotas ir naudojate saugų HTTPS ryšį. Tai ne klaida jūsų autentifikacijos sistemoje – tai fundamentali HTTP protokolo ypatybė, kurią reikia tinkamai valdyti.

Kaip veikia CSRF atakos praktikoje

Pabandykime suprasti mechanizmą be sudėtingų techninių terminų. Tarkime, turite internetinę bankininkystę, kuri leidžia pervesti pinigus paprastu POST užklausa į /transfer endpoint’ą. Kai esate prisijungę, jūsų naršyklė laiko sesijos slapuką, kuris automatiškai pridedamas prie kiekvienos užklausos.

Užpuolikas sukuria nekaltai atrodantį puslapį – galbūt su katinukų nuotraukomis ar „laimėjote prizą” pranešimu. Tame puslapyje paslėpta forma:

<form action="https://jusubankas.lt/transfer" method="POST">
<input type="hidden" name="amount" value="10000">
<input type="hidden" name="to_account" value="uzpuoliko_saskaita">
</form>
<script>document.forms[0].submit();</script>

Kai atidarote šį puslapį, forma automatiškai pateikiama. Jūsų naršyklė, būdama gera ir paklusni, prideda jūsų banko sesijos slapuką prie užklausos. Banko serveris mato galiojantį slapuką ir vykdo pavedimą. Boom – pinigai iškeliavo.

Realybėje atakos gali būti daug subtilesnės. Užpuolikas gali panaudoti <img> tagą GET užklausoms, JavaScript fetch() funkcijas, arba net CSS trikius. Kai kurios atakos net nereikalauja, kad aukos paspaustu mygtuką – pakanka, kad aplankytų puslapį.

Kodėl tradicinės apsaugos priemonės neveikia

Daugelis kūrėjų bando apsiginti nuo CSRF naudodami neveiksmingus metodus. Pažiūrėkime, kas NEVEIKIA ir kodėl:

Slaptažodžio tikrinimas kiekvienai operacijai – teoriškai skamba gerai, bet praktiškai naudotojai jus prakeiks. Be to, jei CSRF ataka veikia, ji gali pateikti ir slaptažodį, jei jis saugomas automatinio užpildymo funkcijoje.

Referer header’io tikrinimas – kai kurios naršyklės neleidžia siųsti Referer antraštės dėl privatumo nustatymų. Be to, yra būdų ją suklastoti arba pašalinti. Nors tai gali būti papildoma apsaugos sluoksnis, pasikliauti tik juo – rizikinga.

POST užklausų naudojimas vietoj GET – tai gera praktika savaime, bet visiškai neapsaugo nuo CSRF. Kaip matėme ankstesniame pavyzdyje, POST užklausas taip pat galima suklastoti.

Tik HTTPS naudojimas – HTTPS apsaugo duomenis persiuntimo metu, bet nieko nedaro prieš CSRF. Užpuolikas neskaito jūsų duomenų – jis tiesiog siunčia savo užklausas su jūsų kredencialais.

Problema ta, kad daugelis šių metodų bando spręsti ne tą problemą. CSRF nėra apie duomenų vagystę ar nesaugų perdavimą – tai apie negalėjimą atskirti autentiškų naudotojo veiksmų nuo suklastotų.

CSRF tokenai – jūsų pagrindinis ginklas

Efektyviausias būdas apsisaugoti nuo CSRF – naudoti unikalius, nenuspėjamus tokenus. Principas paprastas: kiekvienai formai ar state-changing operacijai sugeneruojate atsitiktinę reikšmę, kurią žino tik jūsų serveris ir teisėtas naudotojas. Užpuolikas negali atspėti šio tokeno, todėl negali sukurti galiojančios užklausos.

Štai kaip tai veikia praktikoje:

1. Kai naudotojas užkrauna formą, serveris sugeneruoja unikalų CSRF tokeną
2. Šis tokenas įterpiamas į formą kaip paslėptas laukas
3. Kai forma pateikiama, serveris patikrina, ar gautas tokenas atitinka laukiamą
4. Jei tokenai nesutampa arba tokeno nėra – užklausa atmetama

Implementacija gali atrodyti taip:

<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="a8f7sd98f7sdf98sd7f">
<!-- kiti formos laukai -->
</form>

Serverio pusėje (pavyzdys su Python/Flask):

from flask_wtf.csrf import CSRFProtect

csrf = CSRFProtect(app)

@app.route('/transfer', methods=['POST'])
def transfer():
# Flask automatiškai patikrina CSRF tokeną
# Jei neteisingas - grąžins 400 klaidą
amount = request.form.get('amount')
# ... vykdyti pavedimą

Svarbu suprasti, kad tokenai turi būti:
Nenuspėjami – naudokite kriptografiškai saugius atsitiktinių skaičių generatorius
Unikalūs kiekvienai sesijai – arba net kiekvienai formai
Saugomi serverio pusėje – dažniausiai sesijoje
Validuojami kiekviename POST/PUT/DELETE užklausoje

SameSite slapukų atributas – moderni gynybos linija

Nuo 2020-ųjų metų turime naują ginklą kovoje su CSRF – SameSite slapukų atributą. Tai gana paprasta, bet efektyvi priemonė, kuri nurodo naršyklei, kada siųsti slapukus.

SameSite turi tris galimas reikšmes:

Strict – slapukas siunčiamas TIK kai užklausa ateina iš to paties domeno. Tai reiškia, kad jei kažkas paspaus nuorodą į jūsų svetainę iš el. laiško ar kito puslapio, slapukas nebus išsiųstas. Labai saugus, bet gali sukelti naudojimo problemų.

Set-Cookie: sessionid=abc123; SameSite=Strict; Secure; HttpOnly

Lax – slapukas siunčiamas su top-level navigacija (kai naudotojas tiesiogiai eina į jūsų puslapį), bet ne su įterptomis užklausomis (iframe, fetch, img). Tai geras balansas tarp saugumo ir patogos. Nuo 2020-ųjų tai yra default reikšmė daugelyje naršyklių.

Set-Cookie: sessionid=abc123; SameSite=Lax; Secure; HttpOnly

None – slapukas siunčiamas visada, net cross-site užklausose. Jei naudojate šią reikšmę, PRIVALOTE naudoti Secure atributą (HTTPS).

Set-Cookie: tracking=xyz789; SameSite=None; Secure

Praktiškai, daugumai aplikacijų Lax yra optimalus pasirinkimas. Jis apsaugo nuo daugybės CSRF atakų, tuo pačiu nelabai trukdydamas normaliam naudojimui. Tačiau atminkite – SameSite nėra sidabrinė kulka. Senesnes naršykles jis neapsaugo, o kai kurie atakų scenarijai vis tiek gali veikti.

API ir SPA aplikacijų ypatumai

Jei kuriate modernią Single Page Application (SPA) ar REST/GraphQL API, CSRF apsauga turi kai kurių niuansų. Tradiciniai CSRF tokenai formos laukuose čia neveikia taip pat gerai.

Token’ai header’iuose

Vietoj formos laukų, tokenus galite siųsti HTTP header’iuose. Tai ypač patogu SPA aplikacijoms:

// Gaunamas tokenas iš meta tago arba API endpoint'o
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;

// Siunčiamas su kiekviena užklausa
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ amount: 100, to: 'account123' })
});

Double Submit Cookie pattern

Alternatyvus metodas – saugoti CSRF tokeną slapuke ir reikalauti, kad tas pats tokenas būtų perduotas užklausos body arba header’yje. Serveris tiesiog palygina abi reikšmes:

Set-Cookie: csrf_token=random_value; SameSite=Strict

// JavaScript
fetch('/api/action', {
headers: {
'X-CSRF-Token': getCookie('csrf_token')
}
})

Šis metodas veikia, nes užpuolikas negali perskaityti slapukų reikšmių dėl Same-Origin Policy. Jis gali priversti naršyklę siųsti slapuką, bet negali sužinoti jo reikšmės, kad įterptų į header’į.

JWT ir stateless API

Jei naudojate JWT tokenus localStorage ar sessionStorage, techniškai esate apsaugoti nuo tradicinių CSRF atakų, nes šie saugyklos mechanizmai nėra automatiškai siunčiami su užklausomis. Tačiau čia kyla XSS (Cross-Site Scripting) rizika – jei užpuolikas gali įvykdyti JavaScript jūsų puslapyje, jis gali pasiekti localStorage.

Geriausias požiūris – kombinuoti metodus: naudoti HttpOnly slapukus refresh token’ams ir trumpalaikius access token’us atmintyje.

Framework’ų ir bibliotekų pagalba

Gera žinia ta, kad jums nebūtina visko implementuoti nuo nulio. Dauguma modernių framework’ų turi integruotą CSRF apsaugą.

Django – turi puikią CSRF apsaugą iš dėžės. Tiesiog įtraukite {% csrf_token %} į savo formas:

<form method="post">
{% csrf_token %}
<!-- formos laukai -->
</form>

Django automatiškai sugeneruos ir validuos tokenus. Middleware pasirūpins viskuo.

Ruby on Rails – taip pat turi įmontuotą apsaugą. Tiesiog įsitikinkite, kad jūsų ApplicationController turi:

class ApplicationController < ActionController::Base protect_from_forgery with: :exception end

Express.js (Node.js) – naudokite csurf middleware:

const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

app.post('/transfer', csrfProtection, (req, res) => {
// Užklausa automatiškai validuojama
});

Laravel (PHP) – automatiškai generuoja CSRF tokenus visoms formoms. Naudokite @csrf direktyvą Blade template'uose:

<form method="POST" action="/transfer">
@csrf
<!-- formos laukai -->
</form>

Spring Security (Java) – CSRF apsauga įjungta pagal nutylėjimą. Thymeleaf automatiškai įterpia tokenus:

<form th:action="@{/transfer}" method="post">
<!-- Spring automatiškai pridės CSRF tokeną -->
</form>

Svarbu ne tik įjungti šias funkcijas, bet ir suprasti, kaip jos veikia. Kartais kūrėjai išjungia CSRF apsaugą, nes "kažkas neveikia", o iš tikrųjų problema yra neteisingoje konfigūracijoje arba AJAX užklausų tvarkymo logikoje.

Testavimas ir auditas

Kaip įsitikinti, kad jūsų apsauga tikrai veikia? Štai keletas praktinių būdų:

Rankinis testavimas

Sukurkite paprastą HTML failą su forma, kuri siunčia užklausą į jūsų aplikaciją:

<!DOCTYPE html>
<html>
<body>
<form action="https://jusu-aplikacija.lt/delete-account" method="POST">
<input type="submit" value="Paspausti čia">
</form>
<script>
// Arba automatinis submit'as
document.forms[0].submit();
</script>
</body>
</html>

Atidarykite šį failą lokaliai, būdami prisijungę prie savo aplikacijos kitame tab'e. Jei operacija pavyksta – turite CSRF problemą.

Automatizuoti įrankiai

OWASP ZAP ir Burp Suite turi integruotus CSRF testavimo modulius. Jie automatiškai aptinka formas ir bando jas eksploatuoti be tokenų.

Penetration testing checklist

- Pabandykite pašalinti CSRF tokeną iš užklausos
- Pabandykite naudoti seną/negaliojantį tokeną
- Pabandykite naudoti tokeno reikšmę iš kitos sesijos
- Patikrinkite, ar tokenai tinkamai validuojami visuose endpoint'uose
- Įsitikinkite, kad GET užklausos nekeičia state'o
- Patikrinkite SameSite atributo konfigūraciją

Code review punktai

Peržiūrėdami kodą, atkreipkite dėmesį į:
- Ar visi POST/PUT/DELETE endpoint'ai turi CSRF apsaugą?
- Ar nėra endpoint'ų, kurie išjungia CSRF validaciją be pagrįstos priežasties?
- Ar CSRF tokenai generuojami saugiai?
- Ar tokenai tinkamai saugomi ir validuojami?

Kai apsauga tampa gyvenimo būdu

CSRF apsauga nėra vienkartinis uždavinys, kurį atlikus galima pamiršti. Tai nuolatinis procesas, kuris turi būti integruotas į jūsų development workflow.

Pradėkite nuo pagrindų: įsitikinkite, kad jūsų framework'o CSRF apsauga yra įjungta ir tinkamai sukonfigūruota. Tai jau eliminuos 90% problemų. Pridėkite SameSite=Lax atributą prie sesijos slapukų – tai papildomas apsaugos sluoksnis, kuris kainuoja vieną eilutę kodo.

Jei kuriate API, nepasitikėkite vien CORS apsauga – ji nesaugo nuo CSRF. Naudokite custom header'ius su tokenais arba double submit cookie pattern'ą. O jei galite, apsvarstykit stateless autentifikaciją su JWT, kur tokenai saugomi ne slapukuose.

Svarbiausia – išmokykite savo komandą suprasti, kas yra CSRF ir kodėl tai svarbu. Geriausia apsauga yra ta, kuri suprantama ir nuosekliai taikoma visoje aplikacijoje. Įtraukite CSRF testavimą į savo security checklist'ą, darykite reguliarius auditus, ir niekada neišjunkite apsaugos "laikinai" – žinome, kaip tai baigiasi.

Galiausiai, atminkite: saugumas yra sluoksnių žaidimas. CSRF apsauga yra tik viena dalis platesnės strategijos, kuri turėtų apimti ir XSS prevenciją, ir saugų sesijų valdymą, ir input validaciją. Bet jei šiandien įgyvendinate bent CSRF tokenus ir SameSite slapukus – jūsų aplikacija jau daug saugesnė nei buvo vakar.

Daugiau

Fluentd logų kolektorius