Panduan Pasukan Lucee MY

Rate limiting dalam CFML: had kadar dengan cache, kunci dan respons 429

Bina had kadar dalam CFML dengan cache: tetingkap tetap lawan bergerak, kunci untuk perlumbaan, dan respons 429 dengan Retry-After yang diuji di rig Lucee.

Rate limiting dalam CFML: had kadar dengan cache, kunci dan respons 429
0
Kandungan

Rate limiting dalam CFML: had kadar dengan cache, kunci dan respons 429

Jawapan pantas: Had kadar dalam CFML dibina dengan menyimpan kiraan permintaan bagi setiap kunci, biasanya alamat IP, di dalam cache, kemudian menolak permintaan apabila kiraan melepasi had. Dua perkara menentukan sama ada ia berkesan: bentuk tetingkap masa, dan kunci di sekeliling bacaan serta tulisan kiraan itu. Diukur pada Lucee 7.1.1.17, tanpa kunci had 10 menjadi 40 permintaan yang dibenarkan.

Kenapa had kadar perlu dalam aplikasi, bukan hanya di proksi

Proksi di hadapan Lucee menyekat trafik dengan baik, tetapi ia tidak tahu apa-apa tentang pengguna. Ia melihat satu alamat IP untuk seluruh rangkaian pejabat, dan ia tidak tahu sama ada permintaan itu cubaan log masuk atau bacaan artikel. Had kadar aplikasi boleh mengira mengikut perkara yang proksi tidak nampak: cubaan log masuk bagi satu nama pengguna, atau panggilan API untuk satu token. Jadi kedua-duanya dipasang: proksi menapis banjir trafik, aplikasi mengira kuota yang bermakna.

Bagaimana tetingkap tetap berfungsi dengan cache Lucee?

Masa dibahagi kepada blok, contohnya setiap 60 saat, dan setiap blok mempunyai kiraan sendiri. Kunci cache dibina daripada nama baldi, kunci pelanggan dan nombor blok, jadi kiraan bermula semula secara automatik apabila blok bertukar.

<cfscript>
function hadKadarTetap(string bucket, string kunci, numeric had, numeric saat) {
    var id = arguments.bucket & "|" & arguments.kunci & "|" & int(getTickCount() / 1000 / arguments.saat);
    var bil = cacheGet(local.id);
    if (isNull(local.bil)) local.bil = 0;
    local.bil = local.bil + 1;
    cachePut(local.id, local.bil, createTimeSpan(0, 0, 0, arguments.saat), createTimeSpan(0, 0, 0, arguments.saat));
    var baki = arguments.had - local.bil;
    return { benar: local.bil <= arguments.had, baki: (baki > 0 ? baki : 0) };
}

kunciUjian = "ip-" & createUUID();
lulus = 0;
tolak = 0;
for (i = 1; i <= 12; i++) {
    if (hadKadarTetap("artikel", kunciUjian, 10, 60).benar) lulus = lulus + 1;
    else tolak = tolak + 1;
}
writeOutput("lulus=" & lulus & " tolak=" & tolak);
</cfscript>

Dua belas permintaan dengan had 10 setiap 60 saat menghasilkan lulus=10 tolak=2. Dua perkara dalam kod itu senang tersilap. local. digunakan untuk setiap pemboleh ubah, kerana Lucee menyelesaikan nama tanpa skop kepada arguments dahulu di dalam fungsi, jadi var rekod diikuti rekod boleh membaling The key [REKOD] doesn't exist in the arguments scope. Dan masa diambil daripada getTickCount(), yang mengira milisaat sejak pelayan dimulakan, jadi ia tidak terjejas oleh perubahan waktu.

Kenapa tetingkap tetap membenarkan dua kali had pada sempadan?

Kerana tetingkap tetap tidak melihat ke belakang. Apabila tetingkap baharu bermula, kiraan kembali sifar, jadi pelanggan yang menghabiskan kuota di hujung satu tetingkap boleh menghabiskan kuota kedua sejurus selepas sempadan: satu letusan besar.

<cfscript>
function cuba(string kunci, numeric had, numeric saat) {
    var id = arguments.kunci & "|" & int(getTickCount() / 1000 / arguments.saat);
    var bil = cacheGet(local.id);
    if (isNull(local.bil)) local.bil = 0;
    local.bil = local.bil + 1;
    cachePut(local.id, local.bil, createTimeSpan(0, 0, 0, arguments.saat), createTimeSpan(0, 0, 0, arguments.saat));
    return local.bil <= arguments.had;
}

kunci = "sempadan-" & createUUID();
mula = getTickCount();
lulus = 0;
for (i = 1; i <= 5; i++) if (cuba(kunci, 5, 2)) lulus = lulus + 1;

semasa = int(getTickCount() / 1000 / 2);
while (int(getTickCount() / 1000 / 2) == semasa) sleep(20);

for (i = 1; i <= 5; i++) if (cuba(kunci, 5, 2)) lulus = lulus + 1;
writeOutput("lulus=" & lulus & " ms=" & (getTickCount() - mula));
</cfscript>

Dua larian pada rig memberi lulus=10 ms=426 dan lulus=10 ms=1017. Kiraannya sama, jarak masanya berbeza kerana permulaan ujian jatuh di tempat berlainan dalam tetingkap. Dalam kedua-dua larian, 10 permintaan dibenarkan sedangkan hadnya 5 setiap 2 saat. Untuk laluan log masuk dengan had 5 cubaan seminit, lebihan itu bermakna.

Adakah tetingkap bergerak menyelesaikan masalah itu?

Tetingkap bergerak menyimpan cap masa setiap permintaan yang dibenarkan dan membuang yang lebih tua daripada tempoh tetingkap pada setiap semakan. Keputusan berdasarkan apa yang berlaku dalam 2 saat terakhir, bukan blok masa yang ditetapkan.

<cfscript>
function benarkan(string kunci, numeric had, numeric tetingkapMs) {
    var kini = getTickCount();
    var rekod = cacheGet(arguments.kunci);
    if (isNull(local.rekod)) local.rekod = [];
    var baru = [];
    for (var t in local.rekod) {
        if (t > kini - arguments.tetingkapMs) local.baru.append(t);
    }
    local.baru.append(kini);
    cachePut(arguments.kunci, local.baru, createTimeSpan(0, 0, 0, 10), createTimeSpan(0, 0, 0, 10));
    return arrayLen(local.baru) <= arguments.had;
}

kunci = "bergerak-" & createUUID();
mula = getTickCount();
lulus = 0;
for (i = 1; i <= 5; i++) if (benarkan(kunci, 5, 2000)) lulus = lulus + 1;
sleep(1000);
for (i = 1; i <= 5; i++) if (benarkan(kunci, 5, 2000)) lulus = lulus + 1;
writeOutput("lulus=" & lulus & " ms=" & (getTickCount() - mula));
</cfscript>

Ujian yang sama bentuknya memberi lulus=5 ms=1001: lima permintaan sahaja, bukan sepuluh. Kosnya, senarai cap masa membesar mengikut bilangan permintaan yang dibenarkan, jadi ia perlu pembersihan dan had saiz. Untuk had besar seperti 1,000 permintaan sejam, tetingkap bergerak yang menyimpan setiap cap masa menjadi mahal.

Kenapa had 10 menjadi 40 apabila permintaan tiba serentak?

Kerana cacheGet dan cachePut ialah dua panggilan berasingan, bukan satu operasi atom. Empat puluh utas boleh membaca 0 pada masa yang sama dan menulis 1, masing-masing menyangka ia permintaan pertama.

<cfscript>
application.dHad = 10;
application.dHasil = [];
application.dKunci = "serentak-" & createUUID();
kumpulan = "";

for (i = 1; i <= 40; i++) {
    nama = "ta" & i;
    thread action="run" name=nama {
        nilai = cacheGet(application.dKunci);
        if (isNull(nilai)) nilai = 0;
        sleep(randRange(1, 4));
        nilai = nilai + 1;
        cachePut(application.dKunci, nilai, createTimeSpan(0, 0, 0, 60), createTimeSpan(0, 0, 0, 60));
        lock name="senarai-a" type="exclusive" timeout="5" {
            arrayAppend(application.dHasil, nilai <= application.dHad);
        }
    };
    kumpulan = listAppend(kumpulan, nama);
}
thread action="join" name=kumpulan timeout="30000";
tanpa = 0;
for (b in application.dHasil) if (b) tanpa = tanpa + 1;
writeOutput("tanpaKunci=" & tanpa);
</cfscript>

Blok itu membenarkan tanpaKunci=40 daripada 40 permintaan. Apabila bacaan dan tulisan yang sama dibalut lock name="kira-kadar" type="exclusive", hasilnya denganKunci=10, dan kesemua 40 permintaan selesai dalam 16 hingga 31 milisaat. Penalti itu kecil kerana kerjanya hanya dua panggilan cache. Tanpa kunci, had kadar hanyalah hiasan.

Jadual ringkasan ukuran di rig

UjianHad yang diisytiharHasil sebenar
Tetingkap tetap, 12 permintaan10 setiap 60 saat10 lulus, 2 tolak
Tetingkap tetap merentas sempadan5 setiap 2 saat10 lulus, dalam 426 dan 1017 ms
Tetingkap bergerak5 setiap 2 saat5 lulus, dalam 1001 ms
40 permintaan serentak tanpa kunci1040 lulus
40 permintaan serentak dengan kunci1010 lulus, dalam 16 hingga 31 ms

Bagaimana membalas 429 dengan Retry-After yang betul?

Kod 403 atau 400 mengelirukan klien yang berkelakuan baik, dan membalas 200 memaksa setiap klien menyemak badan.

<cfscript>
cfheader(statuscode="429", statusText="Too Many Requests");
cfheader(name="Retry-After", value="37");
cfheader(name="Content-Type", value="application/json");
writeOutput('{"ralat":"terlalu banyak permintaan","cuba_selepas":37}');
</cfscript>

Kepala respons sebenar daripada rig:

HTTP/1.1 429 
Retry-After: 37
Content-Type: application/json;charset=UTF-8
Content-Length: 56

Nilai Retry-After mesti dikira, bukan diletakkan tetap. Untuk tetingkap tetap, ia detik sehingga tetingkap baharu bermula: (windowId + 1) * saat - nowSec. Untuk tetingkap bergerak, ia masa sehingga cap masa tertua dalam senarai tamat.

Berapa kos semakan, dan stor mana dipilih?

Setiap permintaan membayar satu bacaan dan satu tulisan cache, dan pada rig 1,000 semakan mengambil 2 milisaat. Angka itu sah pada cache dalam ingatan; dengan Redis atau memcached, setiap semakan menjadi panggilan rangkaian.

StorBaik untukHad yang perlu diingat
Struct dalam applicationSatu pelayan, laluan jarang dipanggilHilang apabila aplikasi dimulakan semula
Cache objek LuceeSatu pelayan, kunci banyakSetiap nod mengira sendiri
Redis atau memcachedBeberapa nod berkongsi satu hadSatu panggilan rangkaian setiap semakan
Proksi di tepiBanjir trafik dan pengimbasTidak tahu pengguna atau nama akaun

Jadual itu menerangkan satu perkara yang selalu terlupa: dua nod bermakna dua kali had.

Had kadar yang laman ini sendiri pakai

Laman ini memakai bentuk tetingkap tetap di dalam Security.cfc: kunci cache ialah gabungan nama baldi, kunci pelanggan dan nombor tetingkap, kiraan disimpan dalam struct dalam ingatan, dan setiap bacaan serta tulisan dibalut lock eksklusif. Ada sapuan yang membuang entri luput setiap lima minit supaya struct itu tidak membesar tanpa henti.

Dua akibat patut disebut: kiraan hilang apabila aplikasi dimulakan semula, dan kerana ia dalam ingatan satu bekas, had hanya tepat selagi satu bekas melayani trafik.

Alamat pelanggan pula bukan cgi.remote_addr mentah. Kerana Lucee berada di belakang proksi, kod itu mengambil hop awam paling kanan daripada X-Forwarded-For. Hop paling kiri tidak boleh dipercayai kerana pelanggan boleh menulisnya sendiri.

Kesilapan yang paling kerap

  • Mengunci pada header yang ditulis pelanggan. Jika X-Forwarded-For diambil bulat-bulat, penyerang menukar nilainya setiap permintaan.
  • Mengira permintaan yang berjaya sahaja. Penyerang yang sentiasa gagal tidak pernah dihadkan.
  • Tiada lock. Seperti diukur di atas, had 10 menjadi 40.
  • Tetingkap panjang dengan had kecil. Lima cubaan setiap jam terlalu longgar.

Soalan Lazim

Berapa lama tetingkap yang patut saya pilih untuk laluan log masuk? Lima cubaan setiap lima minit untuk setiap gabungan nama pengguna dan IP ialah permulaan yang munasabah. Yang penting ialah gabungan kunci: mengira mengikut IP sahaja membenarkan serangan daripada banyak IP, dan mengira mengikut akaun sahaja membenarkan penyerang mengunci akaun orang lain.

Bolehkah saya mengira had kadar dalam sesi, bukan dalam cache? Boleh, tetapi sesi hilang apabila pelanggan memadam kuki. Kiraan mesti disimpan pada kunci yang penyerang tidak boleh tukar dengan percuma.

Adakah had kadar perlu untuk setiap laluan? Tidak. Laluan yang mahal dan yang mengesahkan identiti perlu had ketat: log masuk, pendaftaran, reset kata laluan, penjanaan imej, penghantaran mel. Laluan bacaan biasa boleh memakai had longgar.

Kenapa kiraan saya tidak tepat apabila aplikasi berjalan pada dua bekas? Kerana setiap bekas mempunyai cache sendiri, jadi had sebenar menjadi dua kali ganda. Pindahkan kiraan ke Redis, atau bahagi had dengan bilangan bekas.

Adakah cflock memperlahankan permintaan? Dalam ujian di atas, 40 permintaan serentak yang dibalut kunci selesai dalam 16 hingga 31 milisaat. Kunci menjadi mahal hanya apabila kerja berat diletakkan di dalamnya, seperti query pangkalan data atau panggilan HTTP.

Sumber

0 komen

0 komen

Log masuk untuk menjawab atau memberi komen.

Log masuk