A req.ip, amit a támadó tölt ki: az Express trust proxy csapdája

Bekapcsolod a trust proxy-t, mert proxy mögött vagy — és ezzel a req.ip-t a kliens X-Forwarded-For fejlécére bízod. Miért a true a legrosszabb beállítás, mit válassz helyette, és hogyan kerülhető meg vele az egész rate limited.

Ha valaha kiraktál egy Express appot nginx vagy egy load balancer mögé, valószínűleg beírtad ezt az egy sort, és mentél is tovább:

app.set('trust proxy', true)

Onnantól a req.ip a valódi kliens címét mutatta a proxy loopback-címe helyett, a logok értelmesek lettek, a rate limiter kulcsa is stimmelt. Mindenki boldog. Csak épp ezzel az egy sorral a req.ip értékét átadtad annak, aki a kérést küldi — beleértve azt is, aki épp be akar törni. Ez a cikk arról szól, miért a true a lehető legrosszabb választás erre a beállításra, és mit írj helyette.

Diagram: az X-Forwarded-For fejléc 1.2.3.4 és 10.0.0.1 IP-vel; a trust proxy: true a bal szélső, kliens által írt 1.2.3.4-et választja, a trust proxy: 1 a jobb szélső, proxy által hozzáfűzött 10.0.0.1-et, ez lesz a req.ip

Mit csinál valójában a trust proxy

Amikor a klienstől közvetlenül érkezik a kérés, a req.ip egyszerű: az a TCP-kapcsolat másik végének címe, amit a kernel lát. Csakhogy egy reverse proxy mögött ez a cím mindig a proxyé lesz, sosem a felhasználóé. Az igazi kliens IP-jét a proxy egy fejlécbe teszi — a de facto szabvány erre az X-Forwarded-For (röviden XFF).

A trust proxy beállítás pontosan azt mondja meg az Expressnek, hogy mennyire higgyen ennek a fejlécnek. Alapból ki van kapcsolva (false): ilyenkor az Express egyszerűen eldobja az XFF-et, és a req.ip a socket címe marad. Ha bekapcsolod, az Express elkezdi a fejlécből olvasni a klienst. A kérdés csak az, hogy melyik bejegyzést hiszi el belőle.

Az XFF ugyanis egy lista. A szintaxisa kliens, proxy1, proxy2: a bal szélső a legkorábbi (az eredeti kliens), a jobb szélső a hozzád legközelebbi proxy. Ezt jó fejben tartani, mert innen ered az egész probléma.

A true a bal szélső bejegyzést hiszi el

Felraktam egy minimális Express 5-öt (v5.2.1, Node 22), és a req.ip meg a req.ips értékét kiírattam különböző trust proxy beállításokkal, ugyanazokra a kérésekre. A lényeg elfér néhány sorban:

trust proxy: false
  X-Forwarded-For: 1.2.3.4            ->  req.ip = ::ffff:127.0.0.1   (a fejlécet eldobja)

trust proxy: true
  X-Forwarded-For: 1.2.3.4            ->  req.ip = 1.2.3.4
  X-Forwarded-For: 9.9.9.9, 10.0.0.1  ->  req.ip = 9.9.9.9           (a BAL szélső!)

trust proxy: 1
  X-Forwarded-For: 1.2.3.4            ->  req.ip = 1.2.3.4           (nincs valódi proxy: spoofolható!)
  X-Forwarded-For: 9.9.9.9, 10.0.0.1  ->  req.ip = 10.0.0.1          (jobbról az első)

Nézd meg a középső blokkot. A true beállításnál a kérésemet közvetlenül a szerverhez küldtem (semmilyen proxy nem volt előtte), én meg egy sima curl -H-val ráírtam egy tetszőleges XFF-et — és az Express azt szolgálta ki req.ip-ként. A kételemű láncnál a 9.9.9.9-et választotta, ami a lista bal széle. Az Express dokumentációja ezt nem is titkolja: true esetén „a kliens IP-je a bal szélső bejegyzés az X-Forwarded-For fejlécben”. A bal szél viszont pontosan az a rész, amit a kliens ír. A true tehát nem azt jelenti, hogy „megbízom a proxyban”, hanem azt, hogy „megbízom mindenkiben, aki bármit ír ebbe a fejlécbe”.

És itt jön a rész, amit sokan félreértenek: a false ebből a szempontból biztonságosabb, mint a true. A false-nál a req.ip legalább a valódi socket-cím marad (proxy mögött ez a proxy IP-je — pontatlan, de nem hamisítható). A true-nál a req.ip teljes egészében a támadó kezében van. Aki „biztos, ami biztos” alapon kapcsolja be, épp az ellenkezőjét éri el.

Innen egy sor az egész rate limit megkerülése

Ez nem elméleti. A leggyakoribb IP-alapú védelem, a rate limiting, közvetlenül a req.ip-re épül. Fogtam az express-rate-limit-et (v8.6.2), beállítottam percenként 3 kérésre a /login-ra, a trust proxy-t meg true-ra. Aztán kétféleképpen tüzeltem rá.

// limit = 3 / perc, ugyanaz a (spoofolt) IP:
kérés 1: 200   kérés 2: 200   kérés 3: 200   kérés 4: 429   kérés 5: 429

// ugyanaz a limit, de kérésenként új X-Forwarded-For:
203.0.113.1: 200   .2: 200   .3: 200   .4: 200   .5: 200 ... mind 200

Az első sor mutatja, hogy a limiter működik: a negyedik kérésnél jön a 429 Too Many Requests. A második sorban semmi mást nem csináltam, csak minden kéréshez más IP-t írtam az X-Forwarded-For-ba. Mivel a limiter a req.ip-t használja kulcsként, és a req.ip most a fejléc bal széle, minden kérés új „felhasználónak” számít. A limit így nem 3 lett, hanem végtelen. Egy jelszó-brute-force ellen ez a védelem pontosan annyit ér, mint a be nem kapcsolt.

És nem csak a rate limitről van szó. Ugyanez a hamisítható req.ip mérgezi a hozzáférési logokat, félrevezeti az IP-alapú engedélylistákat (allowlist), és bármilyen „csak belső hálózatról” jellegű döntést kijátszhatóvá tesz.

A helyes beállítás: számold meg a proxykat

A trust proxy nem bináris kapcsoló, csak sokan annak használják. Elfogad számot, konkrét IP-t vagy alhálózatot, előre definiált neveket (loopback, linklocal, uniquelocal), és saját függvényt is. A biztonságos használat kulcsa, hogy ne a bal, hanem a jobb oldalról olvasson — onnan, ahol a te infrastruktúrád ír, nem a kliens.

Ha számot adsz meg, az Express jobbról az N. bejegyzést veszi kliensnek a láncból, ahol N a közted és az internet között álló proxyk száma (ezt le is mértem: háromelemű XFF-nél trust proxy: 1 a jobb szélsőt, 2 a jobbról másodikat, 3 a harmadikat adta). A fenti táblázatban ez a 9.9.9.9, 10.0.0.1 láncnál látszik: trust proxy: 1 mellett a jobb szélső 10.0.0.1 lett a req.ip — az, amit a hozzám legközelebbi (egyetlen) proxy fűzött hozzá —, a támadó által beírt 9.9.9.9-et pedig az Express átugorja. Egy nginx előtted: 1; nginx mögött egy cloud load balancer: 2. IP-alapú megadásnál (app.set('trust proxy', '10.0.0.0/8')) az Express jobbról balra halad a láncon, és az első nem megbízható címnél megáll — az lesz a kliens.

A számláláshoz azt kell tudnod, hány saját hop van a lánc jobb végén. Ez néha meglepően fárasztó: egy rosszul beállított hop-szám ugyanúgy hamisíthatóvá teszi a req.ip-t, mert a támadó egyszerűen betölti annyi elemmel a fejlécet, hogy a te „jobbról N.” pozíciód pont az ő adatára essen. Érdemes egy teszt-endpointon kiíratni a req.ip-t és a nyers XFF-et, és valódi kérésen ellenőrizni, hogy tényleg a proxyd címét szűröd-e ki.

A csavar: a hop-szám is csak addig véd, amíg a proxy tényleg hozzáfűz

Itt van az a részlet, ami miatt érdemes volt lemérni ezt az egészet, nem csak elolvasni a doksit. A fenti táblázatban a trust proxy: 1 az egyelemű 1.2.3.4 fejlécre is 1.2.3.4-et adott vissza — pedig ott nem volt igazi proxy. A hop-szám ugyanis abból indul ki, hogy a lánc jobb végét a te proxyd írja. Ha az appod közvetlenül is elérhető az internetről (a proxy megkerülésével), akkor a támadó saját maga állítja elő a teljes láncot, és a hop-szám a hamisított adatra mutat.

Az MDN ezt egy mondatban összefoglalja: ha a szerver közvetlenül is elérhető az internetről — még ha egy megbízható proxy is van előtte —, akkor az X-Forwarded-For egyetlen része sem tekinthető biztonságosnak. Ebből két gyakorlati szabály következik. Egy: a legkülső, hozzád legközelebbi proxynak felül kell írnia a beérkező X-Forwarded-For-t (nem csak hozzáfűzni), különben a kliens által becsempészett elemek a láncban maradnak. Kettő: a származási szervert (origin) tényleg zárd el, hogy csak a proxy érje el — különben az egész trust-lánc kikerülhető.

Ha ez a fejléc-egyveleg (ki fűz hozzá, ki ír felül, hányan) túl törékenynek tűnik, jó okkal: ezért létezik a szabványosított Forwarded fejléc (RFC 7239), ami egy helyen viszi a klienst, a protokollt és a hostot. Ez sem varázsszer — ugyanúgy csak megbízható proxytól érdemes elhinni —, de legalább egyetlen, jól definiált formátum, nem három ad hoc X-Forwarded-*.

Az express-rate-limit már kiabál emiatt

Annyira gyakori ez a hiba, hogy az express-rate-limit külön beépített ellenőrzést kapott rá. Amint a trust proxy true, és a limitert használod, a konzolra ezt kapod (ezt is a saját futtatásom adta):

ValidationError: The Express 'trust proxy' setting is true, which allows
anyone to trivially bypass IP-based rate limiting.
  code: 'ERR_ERL_PERMISSIVE_TRUST_PROXY'

A hozzá tartozó hibakód-oldal pontosan ezt írja le: a true a bal szélső XFF-bejegyzést használja, amit a kliens is beállíthat, így megnyitja a rate limiter megkerülését. Van párja is: az ERR_ERL_UNEXPECTED_X_FORWARDED_FOR akkor jön, ha érkezik XFF, de a trust proxy false — ez általában azt jelzi, hogy a limiter globálisan korlátoz mindenkit egy közös kulcson (a proxy IP-jén), tehát a beállításod nem stimmel. Ha production alatt bármelyiket látod a logban, az nem zaj, hanem a konfiguráció hibája.

Mikor melyiket, és használnám-e élesben

A döntés valójában egyszerű, ha nem kapcsolóként gondolsz rá:

  • false (alapértelmezés) — ha nincs proxy előtted, vagy nincs szükséged a kliens IP-jére. Ne kapcsold be „csak úgy”.
  • szám (hop-count) — a leggyakoribb helyes eset: ismered a proxyk számát, és a legkülső felülírja az XFF-et. Egy nginx előtt: 1.
  • IP / alhálózat / loopback — ha a proxyid címe stabil és ismert, ez a legpontosabb. A dinamikus IP-jű felhős LB-eknél viszont karbantartási teher.
  • true — production, publikus szerveren gyakorlatilag soha. Csak akkor véd, ha a legkülső proxy garantáltan letörli a bejövő XFF-et, de akkor is fölöslegesen kockázatos a hop-számhoz vagy IP-hez képest.

Használnám-e a true-t élesben? Nem. Nincs olyan helyzet, amit ne oldana meg jobban egy konkrét hop-szám vagy IP-lista, és a true egyetlen félreértett proxy-konfignál csendben kinyitja az összes IP-alapú védelmedet. A tanulság nem az, hogy „ne bízz a proxyban”, hanem hogy pontosan egy dologban bízz: a saját infrastruktúrád által írt, jobb oldali bejegyzésben. Minden más a kliens, és a kliens hazudik.

Források

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük