למה (ואיך) להוסיף Rate Limit ליישומים שלכם
מגבלת קצב גישה (Rate Limit) היא המגבלה הכי חשובה שאתם שוכחים להוסיף ליישומים שלכם. בגרסאות שונות שלה המגבלה תגן עליכם ממתקפות מניעת שירות ובכך תעזור לבנות יישומים חסינים יותר.
בהגדרת המגבלות כדאי לקחת בחשבון את המצבים הבאים:
נרצה לזהות ולחסום בקשות "חשודות", למשל ילדים שמריצים כל מיני סקריפטים אוטומטיים שמחפשים חורים ביישום שלנו.
נרצה לחסום בקשות לגיטימיות שמגיעות בקצב מהיר מידי אם השרת שלנו לא מסוגל לעמוד בקצב (וכך להגן על שאר המשתמשים בשרת). לדוגמא נניח שפיצ'ר החיפוש באתר הוא מאוד מורכב וצורך הרבה משאבים, אז נרצה לוודא שאף פעם השרת לא מחפש יותר מ-X מילים באותו הזמן.
נרצה לחסום פעולות מסוימות לפי משתמשים כדי לאכוף התנהגות לפי כללים שהגדרנו. למשל באתר מידע נרצה לאפשר למשתמשים לקרוא את דפי המידע, אבל אם משתמש קורא יותר מדי דפים בדקה אולי הוא בכלל הריץ סקריפט כדי לרוץ ולסרוק אוטומטית את כל האתר - ואת זה לא נרצה לאפשר.
הכלי שאני עובד איתו עבור הסעיף הראשון נקרא fail2ban. הוא רץ ברמת השרת בלי קשר ליישום ספציפי, קורא את קבצי הלוג ומזהה תבניות חריגות. כשמוצא הוא פשוט חוסם את כתובת ה IP הפוגענית. זה הקישור לדף הבית שלהם:
https://www.fail2ban.org/wiki/index.php/Main_Page
שני הסעיפים האחרים כבר קשורים ליישום ולכן הפיתרון אליהם תלוי בשפת הפיתוח והספריה שבחרתם. נראה שתי דוגמאות.
סביבת הפיתוח Flask המאפשרת פיתוח APIs בפייתון מציעה הרחבה בשם FlaskLimiter. הדוגמא שלהם מספרת את רוב הסיפור:
from flask import Flask
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
@app.route("/slow")
@limiter.limit("1 per day")
def slow():
return "24"
@route("/test")
@login_required
@limiter.limit("1 per day", key_func = lambda : current_user.username)
def test_route():
return "42"
@app.route("/fast")
def fast():
return "42"
הגדרת מגבלות ברירת המחדל אומרת שהנתיב fast מוגבל ל 200 פניות ביום ובכל מקרה לא יותר מ-50 פניות בשעה. הגדרה ספציפית שמופיעה לגבי הנתיב slow אומרת שניתן לפנות אליו לא יותר מפעם ביום והגדרה נוספת לגבי הנתיב test כבר לוקחת בחשבון את שם המשתמש ומגבילה כל משתמש להפעיל את הנתיב רק פעם אחת ביום.
לסביבת Rails יש פיתרון בשם rack-attack שלוקח גישה קצת שונה. הנה הדוגמא:
# config/initializers/rack_attack.rb (for rails apps)
Rack::Attack.throttle("requests by ip", limit: 5, period: 2) do |request|
request.ip
end
# Throttle login attempts for a given email parameter to 6 reqs/minute
# Return the email as a discriminator on POST /login requests
Rack::Attack.throttle('limit logins per email', limit: 6, period: 60) do |req|
if req.path == '/login' && req.post?
req.params['email']
end
end
הגדרות rack-attack נכתבות בקובץ איתחול יחיד עבור כל היישום. הבלוק הראשון חוסם גישות לפי כתובת IP כך שלא יתקבלו יותר מ-5 פניות בכל שתי שניות מאותה כתובת. הבלוק השני מגביל ניסיונות התחברות לפי מייל נתון ל 6 בדקה.
היתרון בגישה של Flask Limiter הוא שאנחנו רואים את ההגבלות כל הזמן כחלק מהגדרת הנתיב. יש בזה משהו מאוד נכון כי הגבלת קצב הגישה הרבה פעמים מושפעת ממה הפונקציה עושה. בחירת ערכי defaults טובים (בשתי הגישות) אומרת שאף פעם לא נשכח לשים מגבלת קצב כשיוצרים נתיב חדש.
עובדים בסביבות אחרות ורוצים לשתף איך אתם שמים מגבלת קצב ליישומים שלכם? אשמח לשמוע בתגובות.