Skip to content

Commit ad76bf5

Browse files
committed
Rate limit bypass fix
1 parent 018fb9f commit ad76bf5

File tree

1 file changed

+105
-25
lines changed

1 file changed

+105
-25
lines changed

app.py

Lines changed: 105 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,56 @@ def cleanup_rate_limit_storage():
6666
if not rate_limit_storage[key]:
6767
del rate_limit_storage[key]
6868

69-
def get_client_ip():
70-
"""Get client IP address, considering proxy headers"""
71-
if request.headers.get('X-Forwarded-For'):
72-
return request.headers.get('X-Forwarded-For').split(',')[0].strip()
73-
elif request.headers.get('X-Real-IP'):
74-
return request.headers.get('X-Real-IP')
75-
else:
76-
return request.remote_addr
69+
def is_valid_ip(ip):
70+
"""Validate IP address format"""
71+
import ipaddress
72+
try:
73+
ipaddress.ip_address(ip)
74+
return True
75+
except ValueError:
76+
return False
77+
78+
def get_rate_limiting_identifiers():
79+
"""
80+
Get multiple identifiers for rate limiting without requiring proxy knowledge
81+
Uses defense-in-depth approach instead of perfect IP detection
82+
"""
83+
import hashlib
84+
85+
# 1. ALWAYS use the direct connection IP (most reliable)
86+
remote_ip = request.remote_addr
87+
identifiers = [f"direct_{remote_ip}"]
88+
89+
# 2. If proxy headers exist, use them BUT with lower trust
90+
forwarded_for = request.headers.get('X-Forwarded-For')
91+
if forwarded_for:
92+
# Take first IP from chain
93+
client_ip = forwarded_for.split(',')[0].strip()
94+
if is_valid_ip(client_ip) and client_ip != remote_ip:
95+
identifiers.append(f"proxy_{client_ip}")
96+
97+
real_ip = request.headers.get('X-Real-IP')
98+
if real_ip and is_valid_ip(real_ip) and real_ip != remote_ip:
99+
identifiers.append(f"realip_{real_ip}")
100+
101+
# 3. Create a session-based identifier (harder to spoof)
102+
user_agent = request.headers.get('User-Agent', '')[:100] # Limit length
103+
session_fingerprint = hashlib.md5(f"{remote_ip}:{user_agent}".encode()).hexdigest()[:12]
104+
identifiers.append(f"session_{session_fingerprint}")
105+
106+
return identifiers
107+
108+
def enhanced_rate_limit_check(identifiers, limit):
109+
"""
110+
Check rate limits across multiple identifiers
111+
ANY identifier hitting the limit blocks the request
112+
"""
113+
for identifier in identifiers:
114+
allowed, count, _ = check_rate_limit(identifier, limit)
115+
if not allowed:
116+
return False, identifier, count, limit
117+
118+
return True, None, 0, limit
77119

78120
def check_rate_limit(key, limit):
79121
"""Check if the request should be rate limited"""
@@ -129,6 +171,10 @@ def decorated_function(*args, **kwargs):
129171
return jsonify({
130172
'status': 'error',
131173
'message': f'Rate limit exceeded for IP address. This IP has made {ip_count} requests in the last 3 hours. Limit is {ip_limit} requests per 3 hours.',
174+
'rate_limit_info': {
175+
'limit_type': 'authenticated_ return jsonify({
176+
'status': 'error',
177+
'message': f'Rate limit exceeded for IP address. This IP has made {ip_count} requests in the last 3 hours. Limit is {ip_limit} requests per 3 hours.',
132178
'rate_limit_info': {
133179
'limit_type': 'authenticated_ip',
134180
'current_count': ip_count,
@@ -1674,37 +1720,63 @@ def ai_system_info():
16741720
def ai_rate_limit_status():
16751721
"""
16761722
Check current rate limit status for AI endpoints
1677-
Useful for debugging and transparency
1723+
Now uses secure IP detection to prevent spoofing
16781724
"""
16791725
try:
16801726
cleanup_rate_limit_storage()
1681-
client_ip = get_client_ip()
1727+
ip_identifiers = get_multiple_ip_identifiers()
1728+
remote_ip = request.remote_addr
16821729
current_time = time.time()
16831730

1731+
# Check for potential spoofing attempts
1732+
forwarded_for = request.headers.get('X-Forwarded-For')
1733+
real_ip = request.headers.get('X-Real-IP')
1734+
is_spoofing_attempt = (forwarded_for or real_ip) and not is_trusted_proxy(remote_ip, {
1735+
'127.0.0.1', '::1', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'
1736+
})
1737+
16841738
status = {
16851739
'status': 'success',
1686-
'client_ip': client_ip,
1740+
'security_info': {
1741+
'remote_ip': remote_ip,
1742+
'client_ips': ip_identifiers,
1743+
'spoofing_detected': is_spoofing_attempt,
1744+
'headers_checked': {
1745+
'x_forwarded_for': forwarded_for,
1746+
'x_real_ip': real_ip
1747+
} if is_spoofing_attempt else None
1748+
},
16871749
'rate_limits': {
16881750
'unauthenticated': {
16891751
'limit': UNAUTHENTICATED_LIMIT,
16901752
'window_hours': 3,
1691-
'requests_made': 0
1753+
'requests_made': 0,
1754+
'ip_breakdown': {}
16921755
},
16931756
'authenticated': {
16941757
'limit': AUTHENTICATED_LIMIT,
16951758
'window_hours': 3,
16961759
'user_requests_made': 0,
1697-
'ip_requests_made': 0
1760+
'ip_requests_made': 0,
1761+
'ip_breakdown': {}
16981762
}
16991763
}
17001764
}
17011765

1702-
# Check unauthenticated rate limit
1703-
unauth_key = f"ai_unauth_ip_{client_ip}"
1704-
unauth_count = sum(count for timestamp, count in rate_limit_storage[unauth_key]
1705-
if timestamp > current_time - RATE_LIMIT_WINDOW)
1706-
status['rate_limits']['unauthenticated']['requests_made'] = unauth_count
1707-
status['rate_limits']['unauthenticated']['remaining'] = max(0, UNAUTHENTICATED_LIMIT - unauth_count)
1766+
# Check unauthenticated rate limits for all IP identifiers
1767+
total_unauth_requests = 0
1768+
for ip_addr in ip_identifiers:
1769+
unauth_key = f"ai_unauth_ip_{ip_addr}"
1770+
unauth_count = sum(count for timestamp, count in rate_limit_storage[unauth_key]
1771+
if timestamp > current_time - RATE_LIMIT_WINDOW)
1772+
total_unauth_requests += unauth_count
1773+
status['rate_limits']['unauthenticated']['ip_breakdown'][ip_addr] = {
1774+
'requests_made': unauth_count,
1775+
'remaining': max(0, UNAUTHENTICATED_LIMIT - unauth_count)
1776+
}
1777+
1778+
status['rate_limits']['unauthenticated']['requests_made'] = total_unauth_requests
1779+
status['rate_limits']['unauthenticated']['remaining'] = max(0, UNAUTHENTICATED_LIMIT - total_unauth_requests)
17081780

17091781
# Check if user is authenticated
17101782
auth_header = request.headers.get('Authorization')
@@ -1713,19 +1785,27 @@ def ai_rate_limit_status():
17131785
try:
17141786
user_data = verify_token(token)
17151787
if user_data:
1716-
# Check authenticated rate limits
17171788
user_key = f"ai_auth_user_{user_data['user_id']}"
1718-
ip_key = f"ai_auth_ip_{client_ip}"
17191789

17201790
user_count = sum(count for timestamp, count in rate_limit_storage[user_key]
17211791
if timestamp > current_time - RATE_LIMIT_WINDOW)
1722-
ip_count = sum(count for timestamp, count in rate_limit_storage[ip_key]
1723-
if timestamp > current_time - RATE_LIMIT_WINDOW)
1792+
1793+
# Check IP-based limits for all identifiers
1794+
total_auth_ip_requests = 0
1795+
for ip_addr in ip_identifiers:
1796+
ip_key = f"ai_auth_ip_{ip_addr}"
1797+
ip_count = sum(count for timestamp, count in rate_limit_storage[ip_key]
1798+
if timestamp > current_time - RATE_LIMIT_WINDOW)
1799+
total_auth_ip_requests += ip_count
1800+
status['rate_limits']['authenticated']['ip_breakdown'][ip_addr] = {
1801+
'requests_made': ip_count,
1802+
'remaining': max(0, AUTHENTICATED_LIMIT - ip_count)
1803+
}
17241804

17251805
status['rate_limits']['authenticated']['user_requests_made'] = user_count
1726-
status['rate_limits']['authenticated']['ip_requests_made'] = ip_count
1806+
status['rate_limits']['authenticated']['ip_requests_made'] = total_auth_ip_requests
17271807
status['rate_limits']['authenticated']['user_remaining'] = max(0, AUTHENTICATED_LIMIT - user_count)
1728-
status['rate_limits']['authenticated']['ip_remaining'] = max(0, AUTHENTICATED_LIMIT - ip_count)
1808+
status['rate_limits']['authenticated']['ip_remaining'] = max(0, AUTHENTICATED_LIMIT - total_auth_ip_requests)
17291809
status['authenticated_user'] = {
17301810
'user_id': user_data['user_id'],
17311811
'username': user_data['username']

0 commit comments

Comments
 (0)