Token CSRF yang berkesan dalam CFML: apa yang lulus ujian pada Lucee 7.1
Token CSRF dalam CFML: guna fungsi terbina Lucee, elak perangkap kunci dan perbandingan biasa, kemudian uji dengan enam senario curl yang diukur pada rig ujian.

Kandungan
- Token CSRF yang berkesan dalam CFML: apa yang lulus ujian pada Lucee 7.1
- Kenapa token CSRF yang kelihatan betul masih gagal
- Apakah yang diukur dalam panduan ini?
- Fungsi terbina Lucee: csrfGenerateToken dan csrfVerifyToken
- Perangkap kunci: token berkunci gagal disahkan tanpa kunci yang sama
- Bagaimana menjadikan token sekali guna dengan remove=true
- Bagaimana token dihantar dalam borang dan disahkan pada pelayan
- Kenapa token kosong lulus pengesahan fungsi terbina?
- Bilakah anda perlu menulis token sendiri?
- Enam senario curl dan kod HTTP sebenar
- Adakah kunci SameSite cukup tanpa token CSRF?
- Kenapa token mesti dijana semula selepas log masuk
- Senarai semak sebelum terbit
- Soalan Lazim
- Sumber
Token CSRF yang berkesan dalam CFML: apa yang lulus ujian pada Lucee 7.1
Jawapan pantas: Token CSRF yang berkesan ialah nilai rawak yang disimpan dalam sesi, dihantar dalam badan permintaan, dan disahkan sebelum apa-apa kerja bermula. Lucee 7.1 membawa csrfGenerateToken() dan csrfVerifyToken(), tetapi ujian pada rig menunjukkan tiga perangkap: csrfVerifyToken("") memulangkan true, token berkunci gagal disahkan jika kunci tidak dihantar semula, dan token kekal sah selepas sessionRotate().
Kenapa token CSRF yang kelihatan betul masih gagal
CSRF bukan serangan mencuri data. Ia serangan menyuruh pelayar mangsa menghantar permintaan yang mangsa tidak niatkan. Pelayar menghantar kuki sesi secara automatik kepada laman yang betul, jadi pelayan tidak nampak apa-apa yang luar biasa: permintaan itu datang dengan kuki yang sah. Token memutuskan rantaian ini kerana penyerang tidak boleh membaca nilai token daripada halaman anda.
Kegagalan biasanya berlaku pada satu daripada empat tempat. Token dijana tetapi tidak disahkan pada setiap laluan POST, jadi satu pengendali yang terlupa cukup untuk membuka pintu. Token dihantar melalui URL, lalu ia bocor dalam header Referer dan dalam log pelayan. Token dibandingkan tanpa semakan kewujudan, jadi nilai kosong dilayan seperti nilai sah. Atau token lama masih diterima selepas pengguna log masuk.
Untuk asas konsep CSRF dan cara XSS sering digabungkan dengan serangan ini, rujuk panduan keselamatan aplikasi web CFML. Halaman itu menerangkan pola token penyegerak secara ringkas; halaman ini menguji pelaksanaannya.
Apakah yang diukur dalam panduan ini?
Setiap angka di bawah datang daripada rig ujian dengan imej lucee/lucee:7.1.1.17-RC, pengurusan sesi dihidupkan, dan curl yang membawa balang kuki berasingan untuk setiap sesi.
Dua ukuran asas. getFunctionList() menyenaraikan tepat dua fungsi csrf, iaitu csrfgeneratetoken dan csrfverifytoken. Token csrfGenerateToken() panjangnya 40 aksara huruf kecil dan nombor, manakala generateSecretKey("AES") memulangkan 44 aksara base64 yang menyahkod kepada 32 bait rawak.
Fungsi terbina Lucee: csrfGenerateToken dan csrfVerifyToken
Dokumentasi Lucee menyatakan csrfGenerateToken(key, forceNew) menyediakan token rawak dan menyimpannya dalam sesi, dan csrfVerifyToken(token, key, remove) mengesahkannya terhadap nilai yang disimpan untuk kunci tertentu. Dalam amalan, satu panggilan sudah cukup untuk mula:
<cfscript>
// Satu token setiap sesi, disimpan oleh Lucee sendiri.
token = csrfGenerateToken();
writeOutput("token: " & token & " (" & len(token) & " aksara)<br>");
writeOutput("sahkan: " & csrfVerifyToken(token));
</cfscript>
Output sebenar daripada rig:
token: 3wmkdwgtw58ewct55nfblr9iarw7lmyblp44og6b (40 aksara)
sahkan: true
Panggil csrfGenerateToken() dua kali dalam sesi yang sama dan anda akan menerima token yang sama, kerana fungsi itu menyimpan nilai sedia ada kecuali forceNew=true diberikan. Itu menjadikannya token per sesi, bukan token per permintaan. Untuk memahami di mana nilai itu disimpan dan apa yang berlaku apabila sesi tamat, lihat pengurusan sesi dalam CFML.
Perangkap kunci: token berkunci gagal disahkan tanpa kunci yang sama
Inilah bahagian yang paling mudah tersalah tulis. Apabila anda memberi kunci pada penjanaan, pengesahan tanpa kunci yang sama akan gagal walaupun token itu betul:
<cfscript>
tokenUndian = csrfGenerateToken("borangUndian");
tanpaKunci = csrfVerifyToken(tokenUndian);
denganKunci = csrfVerifyToken(tokenUndian, "borangUndian");
writeOutput("tanpa kunci: " & tanpaKunci & "<br>");
writeOutput("dengan kunci: " & denganKunci);
</cfscript>
Hasil yang diukur:
| Panggilan | Nilai | Maksud |
|---|---|---|
csrfVerifyToken(token) | false | kunci hilang, token tidak dicari |
csrfVerifyToken(token, "borangUndian") | true | kunci sepadan, token diterima |
csrfVerifyToken(token, "kunciLain") | false | kunci berbeza, token tidak dicari |
Kesannya senyap tetapi berbahaya: borang anda sentiasa membalas 403, dan jika semakan itu ditulis tanpa log, anda akan memburu pepijat pada borang sedangkan puncanya satu argumen yang tertinggal. Jika anda menamakan kunci semasa menjana, namakan kunci yang sama semasa mengesahkan.
Bagaimana menjadikan token sekali guna dengan remove=true
Token per sesi boleh diulang. Sesiapa yang dapat membaca satu token, contohnya melalui skrip yang bocor atau sambungan pelayar yang berniat jahat, boleh menghantarnya berulang kali sepanjang sesi. Argumen ketiga remove=true menutup ruang itu, dan kelakuannya boleh diuji dalam dua baris:
<cfscript>
token = csrfGenerateToken("sekali");
gunaPertama = csrfVerifyToken(token, "sekali", true);
gunaKedua = csrfVerifyToken(token, "sekali", true);
writeOutput("guna pertama: " & gunaPertama & "<br>");
writeOutput("guna kedua: " & gunaKedua);
</cfscript>
Output rig: guna pertama: true, guna kedua: false. Tanpa remove, pengesahan kedua tetap true, jadi token itu kekal sah sehingga sesi tamat atau sehingga forceNew=true dijana semula.
Harga yang anda bayar ialah kelayakan semula untuk pengguna. OWASP menyebut bahawa token per permintaan boleh menyusahkan butang "Back", kerana halaman sebelumnya membawa token yang sudah tidak sah. Jika borang anda panjang, gunakan token per sesi dengan forceNew=true selepas peristiwa penting.
Bagaimana token dihantar dalam borang dan disahkan pada pelayan
Bahagian klien hanya perlu satu medan tersembunyi. Jangan letak token dalam URL:
<cfscript>
tokenBorang = csrfGenerateToken("hubungi");
</cfscript>
<cfoutput>
<form method="post" action="hantar.cfm">
<input type="hidden" name="csrfToken" value="#encodeForHTMLAttribute(tokenBorang)#">
<input type="email" name="emel" required>
<button type="submit">Hantar</button>
</form>
</cfoutput>
Pengendali menyemak sebelum kerja lain bermula. Perhatikan dua semakan dalam baris if: panjang token dahulu, kemudian kesahihan.
<cfscript>
diterima = structKeyExists(form, "csrfToken") ? form.csrfToken : "";
if (!len(diterima) || !csrfVerifyToken(diterima, "hubungi", true)) {
cfheader(statusCode = 403, statusText = "Forbidden");
writeOutput("Token tidak sah");
abort;
}
writeOutput("Borang diterima");
</cfscript>
Fail ini membalas 403 tanpa medan csrfToken dan juga 403 apabila medan itu kosong. Semakan len(diterima) bukan hiasan, dan seksyen seterusnya menunjukkan sebabnya.
Kenapa token kosong lulus pengesahan fungsi terbina?
Ini penemuan yang mengubah cara anda menulis semakan. Pada Lucee 7.1.1.17-RC, csrfVerifyToken("") memulangkan true, sama ada token untuk kunci itu sudah dijana atau belum:
<cfscript>
kosong = csrfVerifyToken("");
csrfGenerateToken("kunciUjian");
kosongSelepas = csrfVerifyToken("");
salah = csrfVerifyToken("abcdef");
writeOutput("kosong=" & kosong & "|selepasDijana=" & kosongSelepas & "|salah=" & salah);
</cfscript>
Output rig:
kosong=true|selepasDijana=true|salah=false
Maksudnya pola biasa if (!csrfVerifyToken(form.csrfToken)) akan meluluskan permintaan yang menghantar medan kosong, dan pola structKeyExists(...) ? form.csrfToken : "" menjadikan medan yang tiada sebagai rentetan kosong yang juga lulus. Pengesahan tanpa semakan panjang memberi rasa selamat tanpa perlindungan.
Untuk kes lain, corak semakan kewujudan yang sama diperlukan seperti yang diterangkan dalam memahami skop CFML.
Bilakah anda perlu menulis token sendiri?
Fungsi terbina cukup untuk kebanyakan borang. Tulis sendiri apabila anda mahu token terikat pada nilai lain, contohnya id pesanan, atau mahu menyimpannya dalam jadual supaya kekal sah merentas beberapa pelayan aplikasi. Dalam kes itu, jana dengan generateSecretKey("AES") dan bandingkan dengan MessageDigest.isEqual:
<cfscript>
function samaToken(nilaiDiterima, nilaiDijangka) {
if (isNull(nilaiDiterima) || isNull(nilaiDijangka)) {
return false;
}
var a = toString(nilaiDiterima).getBytes("UTF-8");
var b = toString(nilaiDijangka).getBytes("UTF-8");
return createObject("java", "java.security.MessageDigest").isEqual(a, b);
}
if (!structKeyExists(session, "tokenSendiri")) {
session.tokenSendiri = generateSecretKey("AES");
}
writeOutput("token: " & session.tokenSendiri & "<br>");
writeOutput("sama: " & samaToken(session.tokenSendiri, session.tokenSendiri) & "<br>");
writeOutput("beza: " & samaToken(session.tokenSendiri, "nilai-lain"));
</cfscript>
Pada rig, blok itu mencetak token 44 aksara, kemudian sama: true dan beza: false. MessageDigest.isEqual tersedia dalam Lucee tanpa sambungan tambahan. Saya tidak mengukur perbezaan masa antara == dan isEqual melalui HTTP kerana hingar rangkaian lebih besar daripada perbezaan itu, jadi ayat ini bukan angka masa. Yang jujur ialah isEqual direka untuk perbandingan bait tanpa berhenti awal, manakala perbandingan rentetan biasa tidak memberi jaminan itu.
Enam senario curl dan kod HTTP sebenar
Saya menjalankan enam senario dengan balang kuki berasingan, dan ini hasil sebenar:
| Senario | Kod HTTP | Badan balasan |
|---|---|---|
| Token betul, sesi yang sama | 200 | 200 diterima |
| Token yang sama dihantar semula | 403 | 403 token tidak sah |
| Tiada medan token | 403 | 403 token tidak sah |
| Token salah bentuk | 403 | 403 token tidak sah |
| Token sah milik sesi lain | 403 | 403 token tidak sah |
Token sah dengan header Origin asing | 200 | 200 diterima |
Baris terakhir itu yang paling mudah disalah faham. Pelayan menerima permintaan yang membawa Origin: https://penyerang.contoh kerana tokennya sah, jadi header Origin tidak diperiksa dalam ujian itu. Jangan gantikan token dengan semakan Origin sahaja. Jika anda membina API untuk JavaScript dari domain lain, semakan asal ada tempatnya, dan CORS dalam CFML membincangkan hadnya.
Adakah kunci SameSite cukup tanpa token CSRF?
Tidak, untuk kebanyakan pemasangan. OWASP menulis bahawa SameSite berguna sebagai lapisan tambahan tetapi tidak menggantikan pertahanan CSRF yang betul, dan Lax hanya menyekat kaedah tidak selamat, jadi pautan biasa masih boleh mencetuskan GET yang mengubah keadaan jika aplikasi anda membenarkannya.
Kesimpulannya mudah. Tetapkan SameSite=Lax atau Strict pada kuki sesi sebagai lapisan kedua, pastikan setiap laluan yang mengubah keadaan menggunakan POST dengan token, dan jangan benarkan operasi tulis melalui GET.
Kenapa token mesti dijana semula selepas log masuk
Ini diukur, bukan diandaikan. Saya menjana token, memanggil sessionRotate(), kemudian mengesahkan token yang sama. Hasilnya: true sebelum putaran, true selepas putaran, dan token yang dijana semula sama dengan token asal. Maksudnya sessionRotate() menukar id sesi tetapi mengekalkan data sesi, termasuk token CSRF.
Jadi selepas log masuk yang berjaya, jangan bergantung pada putaran sesi sahaja. Jana token baharu dengan csrfGenerateToken(forceNew=true) selepas sessionRotate(). Tanpa langkah itu, token yang dipegang penyerang sebelum log masuk masih sah selepas mangsa log masuk.
Senarai semak sebelum terbit
- Setiap laluan POST, PUT dan DELETE memanggil pengesahan sebelum kerja lain.
- Panjang token diperiksa dahulu, kerana
csrfVerifyToken("")memulangkantrue. - Token dihantar dalam badan permintaan, bukan dalam URL.
- Nilai yang tiada dilayan sebagai gagal, bukan sebagai lulus.
- Jika kunci dinamakan semasa menjana, kunci yang sama dihantar semasa mengesahkan.
- Token dijana semula dengan
forceNew=trueselepas log masuk. - Borang penting diuji dengan curl: tanpa token 403, dengan token 200, token sesi lain 403.
- Kuki sesi membawa
SameSitedanSecureapabila laman berjalan melalui HTTPS.
Soalan Lazim
Adakah token CSRF perlu berubah pada setiap permintaan?
Tidak semestinya. Token per sesi dengan forceNew=true selepas peristiwa penting sudah cukup untuk kebanyakan borang. Token sekali guna dengan remove=true lebih ketat, tetapi ia menyusahkan pengguna yang menekan butang Back.
Kenapa csrfVerifyToken memulangkan false walaupun token betul?
Kemungkinan besar kunci tidak sepadan. Jika token dijana dengan csrfGenerateToken("borangUndian"), pengesahan mesti menyebut kunci yang sama; pada rig, pengesahan tanpa kunci memulangkan false dalam sesi yang sama.
Bolehkah saya simpan token CSRF dalam kuki sahaja? OWASP menasihatkan agar tidak bergantung pada pola kuki hantar dua kali yang tidak ditandatangani, kerana penyerang yang boleh menetapkan kuki pada subdomain boleh menetapkan kedua-dua nilai. Jika anda perlukan pola tanpa keadaan, gunakan versi bertandatangan yang terikat pada sesi.
Apa berlaku pada token selepas sessionRotate?
Pada Lucee 7.1.1.17-RC, token kekal sama dan masih sah selepas sessionRotate(). Jana semula dengan forceNew=true selepas log masuk supaya token lama tidak boleh digunakan semula.
Adakah csrfVerifyToken selamat digunakan tanpa semakan tambahan?
Tidak. Pada Lucee 7.1.1.17-RC, csrfVerifyToken("") memulangkan true sebelum dan selepas token dijana untuk kunci itu. Semak len(token) dahulu, atau bandingkan sendiri dengan MessageDigest.isEqual.
Sumber
- OWASP, Cross-Site Request Forgery Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
- Dokumentasi Lucee, CSRFGenerateToken(): https://docs.lucee.org/reference/functions/csrfgeneratetoken.html
- Dokumentasi Lucee, CSRFVerifyToken(): https://docs.lucee.org/reference/functions/csrfverifytoken.html