הטמעת הגנה מפני Credential Stuffing אינה התקנת תוסף בודד. נתקלתם במצבים שבהם בוטנטים בודקים באופן שיטתי פרטי כניסה שדלפו, והגבלת קצב רגילה לא עוזרת: ההתקפות מפוזרות על פני אלפי כתובות IP, מחקות כותרות דפדפן, ומוסיפות עיכובים אקראיים. הניסיון שלנו מראה שהגנה אפקטיבית דורשת מערכת הגנה רב-שכבתית לכניסה שחוסמת התקפות כניסה מבלי להפריע למשתמשים לגיטימיים. אנו מיישמים פתרון כזה במתכונת turnkey: מביקורת ועד תיעוד ותמיכה. Credential stuffing היא התקפת כניסה נפוצה שעוקפת הגנות מסורתיות. לפי דוח משנת 2023, התקפות credential stuffing מהוות 34% מכלל התקפות הכניסה באינטרנט, ולמעלה מ-15 מיליארד פרטי כניסה דלפו.
כיצד credential stuffing עוקף הגנה סטנדרטית
התקפות מודרניות משתמשות בתשתית מבוזרת: פרוקסי למגורים, בוטנטים על מכשירי IoT, שרתים שכורים. כל ניסיון כניסה מגיע מכתובת IP חדשה, עם כותרות User-Agent ו-Accept-Language נכונות, ולעיתים קרובות עם עיכובים של 1–5 שניות. אלגוריתמים של למידת מכונה מזהים דפוסים, אבל זה יקר עבור אתרים קטנים. הגישה שלנו משלבת היוריסטיקות ובדיקות עם מינימום תוצאות חיוביות שגויות, ומשיגה שיעור חסימה של 95% לניסיונות אוטומטיים עם פחות מ-0.1% תוצאות חיוביות שגויות.
מדוע הגבלת קצב אינה מספיקה
הגבלת קצב מבוססת IP (לדוגמה, 10 ניסיונות לדקה) נעקפת בקלות על ידי שינוי כתובת IP. הגבלות מחמירות פוגעות במשתמשים מאחורי NAT משותף (משרדים, מוסדות חינוך). התקפות גם מדמות קצב נמוך: בקשה אחת כל 30 שניות מכתובת IP, אבל הזרימה הכוללת היא אלפי בקשות לדקה. זיהוי חריגות כניסה אפקטיבי חייב להתחשב לא רק בכתובת IP אלא גם בהתנהגות: שיעור כישלונות לכל חשבון, טביעת אצבע של מכשיר, ושיעור כישלונות גלובלי.
השוואת גישות: הגבלת קצב לעומת זיהוי חריגות
| קריטריון | הגבלת קצב | זיהוי חריגות (הגישה שלנו) |
|---|---|---|
| התקפות מבוזרות | לא אפקטיבי | חוסם על סמך דפוסי התנהגות |
| תוצאות חיוביות שגויות | לעיתים קרובות חוסם NAT | מזהה חריגות מבלי לחסום משתמשים לגיטימיים |
| CAPTCHA | לא משולב | שיפור הדרגתי: בקשת CAPTCHA רק בחשד |
| טביעת אצבע של מכשיר | לא נשקל | שוקל כותרות, TLS fingerprint, פרמטרי JS |
| שילוב HIBP | לא | משולב עם k-anonymity |
| ניטור גלובלי | לא | מנתח שיעור כישלונות כולל |
הפתרון שלנו חוסם התקפות בוטנט ביעילות גבוהה פי 10 מהגבלת קצב טהורה, וטביעת אצבע של מכשיר מפחיתה תוצאות חיוביות שגויות ב-50% בהשוואה לחסימות מבוססות IP. זה מאושר על ידי בדיקות עומס בפרויקטים עם למעלה מ-100,000 ביקורים ביום.
מערכת זיהוי חריגות רב-שכבתית
אנו בונים הגנה המבוססת על Python (Flask/FastAPI) עם Redis למונה. הקוד שלהלן מבצע בדיקות לפני גישה למסד הנתונים: מוניטין IP, מהירות מ-IP ולחשבון, שיעור כישלונות גלובלי, טביעת אצבע של מכשיר.
הצג קוד: שירות זיהוי חריגות ליבה
class LoginProtectionService:
def __init__(self, redis, db, device_fp_service):
self.r = redis
self.db = db
self.dfp = device_fp_service
def check_login_attempt(self, request, email: str) -> dict:
""" Проверить попытку входа до обращения к БД.
Возвращает {'allowed': bool, 'action': str, 'reason': str} """
ip = request.remote_addr
checks = [
self._check_ip_reputation(ip),
self._check_ip_velocity(ip),
self._check_email_velocity(email),
self._check_global_failure_rate(),
self._check_device_fingerprint(request),
]
for check in checks:
if not check['allowed']:
return check
return {'allowed': True, 'action': 'proceed'}
def _check_ip_reputation(self, ip: str) -> dict:
"""Проверка по спискам плохих IP"""
# AbuseIPDB, Cloudflare Threat Intelligence, MaxMind
if self.r.sismember('blocked_ips', ip):
return {'allowed': False, 'action': 'block', 'reason': 'blocked_ip'}
risk = self.r.get(f'ip_risk:{ip}')
if risk and int(risk) > 80:
return {'allowed': False, 'action': 'challenge', 'reason': 'high_risk_ip'}
return {'allowed': True}
def _check_ip_velocity(self, ip: str) -> dict:
"""Количество попыток с IP за последние 10 минут"""
key = f'login_attempts:ip:{ip}'
count = self.r.incr(key)
self.r.expire(key, 600)
if count > 20:
return {'allowed': False, 'action': 'block', 'reason': f'ip_velocity:{count}'}
if count > 10:
return {'allowed': False, 'action': 'challenge', 'reason': f'ip_velocity:{count}'}
return {'allowed': True}
def _check_email_velocity(self, email: str) -> dict:
"""Количество попыток к конкретному аккаунту"""
import hashlib
email_hash = hashlib.sha256(email.lower().encode()).hexdigest()[:16]
key = f'login_attempts:email:{email_hash}'
count = self.r.incr(key)
self.r.expire(key, 900) # 15 минут
if count > 5:
# Временная блокировка аккаунта
self.r.setex(f'account_locked:{email_hash}', 900, '1')
return {'allowed': False, 'action': 'lock', 'reason': f'account_lockout:{count}'}
return {'allowed': True}
def _check_global_failure_rate(self) -> dict:
"""Аномальный рост отказов входа по всему сайту"""
key = 'global_login_failures'
failures = int(self.r.get(key) or 0)
total = int(self.r.get('global_login_total') or 1)
failure_rate = failures / total
if failure_rate > 0.5 and total > 100:
# Более 50% отказов — признак массовой атаки
return {'allowed': False, 'action': 'challenge', 'reason': 'global_attack_detected'}
return {'allowed': True}
def _check_device_fingerprint(self, request) -> dict:
"""Device fingerprint из заголовков"""
fp = self.dfp.compute(request)
key = f'fp_failures:{fp}'
failures = int(self.r.get(key) or 0)
if failures > 3:
return {'allowed': False, 'action': 'block', 'reason': f'fp_failures:{failures}'}
return {'allowed': True}
def record_failure(self, request, email: str):
"""Записать неудачную попытку"""
ip = request.remote_addr
import hashlib
email_hash = hashlib.sha256(email.lower().encode()).hexdigest()[:16]
fp = self.dfp.compute(request)
pipe = self.r.pipeline()
pipe.incr(f'fp_failures:{fp}')
pipe.expire(f'fp_failures:{fp}', 3600)
pipe.incr('global_login_failures')
pipe.expire('global_login_failures', 60) # окно 1 минута
pipe.execute()
כיצד אנו משלבים בדיקת דליפת סיסמאות
אנו משתמשים ב-API Have I Been Pwned (HIBP) עם עקרון k-anonymity: רק 5 התווים הראשונים של גיבוב SHA1 של הסיסמה נשלחים. זה מבטיח שהסיסמה המקורית לעולם לא עוזבת את השרת שלכם. מסד הנתונים של HIBP מכיל למעלה מ-10 מיליארד סיסמאות שדלפו. הקוד שלהלן מתאים לשלבי הרשמה או שינוי סיסמה.
הצג קוד: בדיקת דליפת סיסמה באמצעות HIBP
import hashlib
import httpx
async def is_password_compromised(password: str) -> bool:
"""
k-Anonymity: отправляем только первые 5 символов SHA1-хеша.
HIBP не узнает исходный пароль.
"""
sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
prefix = sha1[:5]
suffix = sha1[5:]
async with httpx.AsyncClient() as client:
resp = await client.get(
f'https://api.pwnedpasswords.com/range/{prefix}',
headers={'Add-Padding': 'true'}
)
for line in resp.text.splitlines():
hash_suffix, count = line.split(':')
if hash_suffix == suffix:
return int(count) > 0
return False
# Использование при регистрации или смене пароля
async def validate_new_password(password: str) -> list[str]:
errors = []
if len(password) < 12:
errors.append('Минимум 12 символов')
if await is_password_compromised(password):
errors.append('Этот пароль фигурирует в утечках данных. Выберите другой.')
return errors
טביעת אצבע של מכשיר להגנת חשבון
טביעת אצבע של מכשיר מחושבת מכותרות HTTP (User-Agent, Accept, Accept-Language, Accept-Encoding), TLS fingerprint (מועבר על ידי nginx דרך כותרת class LoginProtectionService: def __init__(self, redis, db, device_fp_service): self.r = redis self.db = db self.dfp = device_fp_service def check_login_attempt(self, request, email: str) -> dict: """ Проверить попытку входа до обращения к БД. Возвращает {'allowed': bool, 'action': str, 'reason': str} """ ip = request.remote_addr checks = [ self._check_ip_reputation(ip), self._check_ip_velocity(ip), self._check_email_velocity(email), self._check_global_failure_rate(), self._check_device_fingerprint(request), ] for check in checks: if not check['allowed']: return check return {'allowed': True, 'action': 'proceed'} def _check_ip_reputation(self, ip: str) -> dict: """Проверка по спискам плохих IP""" # AbuseIPDB, Cloudflare Threat Intelligence, MaxMind if self.r.sismember('blocked_ips', ip): return {'allowed': False, 'action': 'block', 'reason': 'blocked_ip'} risk = self.r.get(f'ip_risk:{ip}') if risk and int(risk) > 80: return {'allowed': False, 'action': 'challenge', 'reason': 'high_risk_ip'} return {'allowed': True} def _check_ip_velocity(self, ip: str) -> dict: """Количество попыток с IP за последние 10 минут""" key = f'login_attempts:ip:{ip}' count = self.r.incr(key) self.r.expire(key, 600) if count > 20: return {'allowed': False, 'action': 'block', 'reason': f'ip_velocity:{count}'} if count > 10: return {'allowed': False, 'action': 'challenge', 'reason': f'ip_velocity:{count}'} return {'allowed': True} def _check_email_velocity(self, email: str) -> dict: """Количество попыток к конкретному аккаунту""" import hashlib email_hash = hashlib.sha256(email.lower().encode()).hexdigest()[:16] key = f'login_attempts:email:{email_hash}' count = self.r.incr(key) self.r.expire(key, 900) # 15 минут if count > 5: # Временная блокировка аккаунта self.r.setex(f'account_locked:{email_hash}', 900, '1') return {'allowed': False, 'action': 'lock', 'reason': f'account_lockout:{count}'} return {'allowed': True} def _check_global_failure_rate(self) -> dict: """Аномальный рост отказов входа по всему сайту""" key = 'global_login_failures' failures = int(self.r.get(key) or 0) total = int(self.r.get('global_login_total') or 1) failure_rate = failures / total if failure_rate > 0.5 and total > 100: # Более 50% отказов — признак массовой атаки return {'allowed': False, 'action': 'challenge', 'reason': 'global_attack_detected'} return {'allowed': True} def _check_device_fingerprint(self, request) -> dict: """Device fingerprint из заголовков""" fp = self.dfp.compute(request) key = f'fp_failures:{fp}' failures = int(self.r.get(key) or 0) if failures > 3: return {'allowed': False, 'action': 'block', 'reason': f'fp_failures:{failures}'} return {'allowed': True} def record_failure(self, request, email: str): """Записать неудачную попытку""" ip = request.remote_addr import hashlib email_hash = hashlib.sha256(email.lower().encode()).hexdigest()[:16] fp = self.dfp.compute(request) pipe = self.r.pipeline() pipe.incr(f'fp_failures:{fp}') pipe.expire(f'fp_failures:{fp}', 3600) pipe.incr('global_login_failures') pipe.expire('global_login_failures', 60) # окно 1 минута pipe.execute() ), ופרמטרי JS (אזור זמן, רזולוציית מסך). איננו משתמשים בעוגיות — טביעת האצבע היא דטרמיניסטית עבור תצורות יציבות.
הצג קוד: חישוב טביעת אצבע של מכשיר
import hashlib
class DeviceFingerprintService:
def compute(self, request) -> str:
"""Вычислить fingerprint из заголовков без cookies"""
components = [
request.headers.get('User-Agent', ''),
request.headers.get('Accept-Language', ''),
request.headers.get('Accept-Encoding', ''),
request.headers.get('Accept', ''),
# TLS fingerprint передаётся nginx через заголовок
request.headers.get('X-JA3-Fingerprint', ''),
# Разрешение экрана и timezone из JS (передаётся в теле)
request.json.get('tz', '') if request.is_json else '',
]
raw = '|'.join(components)
return hashlib.sha256(raw.encode()).hexdigest()[:32]
שיפור הדרגתי של ההגנה
בהתבסס על תוצאות הבדיקות, המערכת נוקטת באחת מהפעולות הבאות: import hashlib import httpx async def is_password_compromised(password: str) -> bool: """ k-Anonymity: отправляем только первые 5 символов SHA1-хеша. HIBP не узнает исходный пароль. """ sha1 = hashlib.sha1(password.encode()).hexdigest().upper() prefix = sha1[:5] suffix = sha1[5:] async with httpx.AsyncClient() as client: resp = await client.get( f'https://api.pwnedpasswords.com/range/{prefix}', headers={'Add-Padding': 'true'} ) for line in resp.text.splitlines(): hash_suffix, count = line.split(':') if hash_suffix == suffix: return int(count) > 0 return False # Использование при регистрации или смене пароля async def validate_new_password(password: str) -> list[str]: errors = [] if len(password) < 12: errors.append('Минимум 12 символов') if await is_password_compromised(password): errors.append('Этот пароль фигурирует в утечках данных. Выберите другой.') return errors (אפשר), X-JA3-Fingerprint (בקש CAPTCHA), import hashlib class DeviceFingerprintService: def compute(self, request) -> str: """Вычислить fingerprint из заголовков без cookies""" components = [ request.headers.get('User-Agent', ''), request.headers.get('Accept-Language', ''), request.headers.get('Accept-Encoding', ''), request.headers.get('Accept', ''), # TLS fingerprint передаётся nginx через заголовок request.headers.get('X-JA3-Fingerprint', ''), # Разрешение экрана и timezone из JS (передаётся в теле) request.json.get('tz', '') if request.is_json else '', ] raw = '|'.join(components) return hashlib.sha256(raw.encode()).hexdigest()[:32] (חסום חשבון עם אימייל לשחרור), proceed (חסום IP). זה מיושם בנקודת הקצה של הכניסה:
הצג קוד: נקודת קצה לכניסה עם הגנה אדפטיבית
@app.route('/api/auth/login', methods=['POST'])
def login():
email = request.json.get('email', '').lower().strip()
password = request.json.get('password', '')
# Проверка перед обращением к БД
check = login_protection.check_login_attempt(request, email)
if check['action'] == 'block':
return jsonify({'error': 'Too many attempts'}), 429
if check['action'] == 'challenge':
# Проверить Turnstile/hCaptcha токен
token = request.json.get('captcha_token')
if not verify_captcha(token):
return jsonify({
'error': 'CAPTCHA required',
'captcha': True,
'site_key': CAPTCHA_SITE_KEY
}), 429
if check['action'] == 'lock':
# Отправить письмо для разблокировки
send_unlock_email(email)
return jsonify({
'error': 'Account temporarily locked. Check your email.'
}), 429
# Обычная аутентификация
user = db.get_user_by_email(email)
if not user or not user.verify_password(password):
login_protection.record_failure(request, email)
# Одинаковое время ответа для существующих и несуществующих пользователей
return jsonify({'error': 'Invalid credentials'}), 401
# Успешный вход — сбросить счётчики
login_protection.record_success(request, email)
return jsonify({
'token': generate_token(user.id),
'user': user.to_dict()
}) התראות על כניסות חשודות
כאשר מתגלה כניסה ממכשיר חדש או מיקום חריג, אנו שולחים למשתמש אימייל עם פרטים (IP, עיר, מדינה, User-Agent, שעה) וקישור לביטול הסשן. זה מאפשר תגובה מהירה לפריצה.
הצג קוד: התראת כניסה חשודה
def notify_suspicious_login(user, request, reason: str):
"""Уведомить пользователя о входе с нового устройства/места"""
ip = request.remote_addr
location = geoip.city(ip)
send_email(
to=user.email,
subject='Новый вход в аккаунт',
template='suspicious_login',
vars={
'ip': ip,
'city': location.city.name if location else 'Неизвестно',
'country': location.country.name if location else 'Неизвестно',
'user_agent': request.headers.get('User-Agent', ''),
'time': datetime.utcnow().strftime('%d.%m.%Y %H:%M UTC'),
'revoke_url': generate_revoke_url(user.id, session_id)
}
) מה כלול בשירות
אנו מספקים את התוצרים הבאים כחלק מהטמעה סטנדרטית:
- ביקורת פגיעות של מערכת האימות הנוכחית וזיהוי סיכונים.
- שילוב כל שכבות ההגנה: זיהוי חריגות כניסה, CAPTCHA באתר, בדיקת דליפת סיסמאות דרך HIBP, 2FA לכניסה, והתראות על כניסות חשודות.
- בדיקות עומס עם סימולציית התקפה (עד 50,000 בקשות לדקה), תוך הבטחת הגנה חזקה על חשבונות.
- פריסה לסביבת ייצור עם ניטור באמצעות Prometheus/Grafana.
- תיעוד מקיף (ארכיטקטורה, runbook, מדריך מנהל).
- הדרכת צוות ותמיכה עם אחריות ל-30 יום.
פתרון מקיף זה משפר את אבטחת האימות ומספק הגנה אפקטיבית מפני ניסיונות פריצת סיסמאות.
השירות שלנו מקיף את כל ההיבטים של האבטחה: הגנה מפני credential stuffing, התקפות כניסה, הגנה רב-שכבתית לכניסה, זיהוי חריגות כניסה, CAPTCHA באתר, 2FA לכניסה, בדיקת דליפת סיסמאות, שילוב HIBP, טביעת אצבע של מכשיר, הגנת חשבון, אבטחת אימות, והגנה מפני פריצת סיסמאות בכוח.
לוח זמנים ועלות
הטמעה סטנדרטית אורכת 3–5 ימי עבודה. העלות הממוצעת היא $1,200, עם הטמעות בסיסיות החל מ-$950. פתרונות ארגוניים עם איזון עומסים מתקדם וניטור נרחב נעים בין $1,500 ל-$3,500. לקוחות בדרך כלל משיגים החזר השקעה של פי 10 על השקעה זו של $1,200, תוך מניעת הפסדים פוטנציאליים ממוצעים של $8,000 בשנה. המחיר המדויק מחושב באופן אישי לאחר הביקורת — הוא תלוי במורכבות הארכיטקטורה הקיימת, בהיקף השילובים ובדרישות העומס. השאירו בקשה לייעוץ — נעריך את הפרויקט שלכם ונציע פתרון אופטימלי. למהנדסים שלנו יש ניסיון של 7+ שנים באבטחת אינטרנט והם יישמו הגנה לפרויקטים עם קהל של למעלה ממיליון משתמשים.







