Limit authentication attempt per IP network

ip_address and ip_network are added to LoginAttempt.

issue7110
review41031002
This commit is contained in:
Cédric Krier
2018-02-14 19:24:01 +01:00
parent e8ee8dedac
commit fbf488948b
6 changed files with 71 additions and 2 deletions
+1
View File
@@ -1,3 +1,4 @@
* Limit authentication attempt per IP network
* Manage 'X-Forwarded' headers from proxies
* Add '_request' attribute to Transaction context
* Delete translations on deletion only when model has translatable field
+25
View File
@@ -262,6 +262,31 @@ attempts over a period of `timeout`.
Default: `5`
max_attempt_ip_network
~~~~~~~~~~~~~~~~~~~~~~
The maximum authentication attempt from the same network before the server
answers unconditionally `Too Many Requests` for any other attempts. The
counting is done on all attempts over a period of `timeout`.
Default: `300`
ip_network_4
~~~~~~~~~~~~
The network prefix to apply on IPv4 address for counting the authentication
attempts.
Default: `32`
ip_network_6
~~~~~~~~~~~~
The network prefix to apply on IPv6 address for counting the authentication
attempts.
Default: `56`
password
--------
+1
View File
@@ -16,6 +16,7 @@ Prerequisites
* python-dateutil (http://labix.org/python-dateutil)
* polib (https://bitbucket.org/izi/polib/wiki/Home)
* python-sql 0.4 or later (http://code.google.com/p/python-sql/)
* ipaddress (https://github.com/phihag/ipaddress)
* Optional: psycopg 2.5.0 or later (http://www.initd.org/)
* Optional: psycopg2cffi 2.5.0 or later
(http://github.com/chtd/psycopg2cffi)
+1
View File
@@ -100,6 +100,7 @@ setup(name=name,
'python-sql >= 0.4',
'werkzeug',
'wrapt',
'ipaddress',
],
extras_require={
'PostgreSQL': pg_require,
+3
View File
@@ -67,6 +67,9 @@ class TrytonConfigParser(ConfigParser.RawConfigParser):
self.set('session', 'authentications', 'password')
self.set('session', 'timeout', 600)
self.set('session', 'max_attempt', 5)
self.set('session', 'max_attempt_ip_network', 300)
self.set('session', 'ip_network_4', 32)
self.set('session', 'ip_network_6', 56)
self.add_section('password')
self.set('password', 'length', 8)
self.set('password', 'entropy', 0.75)
+40 -2
View File
@@ -11,6 +11,7 @@ import datetime
import logging
import uuid
import mmap
import ipaddress
from functools import wraps
from itertools import groupby, ifilter
from operator import attrgetter
@@ -540,6 +541,11 @@ class User(ModelSQL, ModelView):
Return user id if password matches
'''
LoginAttempt = Pool().get('res.user.login.attempt')
count_ip = LoginAttempt.count_ip()
if count_ip > config.getint(
'session', 'max_attempt_ip_network', default=300):
# Do not add attempt as the goal is to prevent flooding
raise RateLimitException()
count = LoginAttempt.count(login)
if count > config.getint('session', 'max_attempt', default=5):
LoginAttempt.add(login)
@@ -634,6 +640,8 @@ class LoginAttempt(ModelSQL):
"""
__name__ = 'res.user.login.attempt'
login = fields.Char('Login', size=512)
ip_address = fields.Char("IP Address")
ip_network = fields.Char("IP Network")
@classmethod
def __register__(cls, module_name):
@@ -649,6 +657,21 @@ class LoginAttempt(ModelSQL):
return (datetime.datetime.now()
- datetime.timedelta(seconds=config.getint('session', 'timeout')))
@classmethod
def ipaddress(cls):
context = Transaction().context
ip_address = ''
ip_network = ''
if context.get('_request') and context['_request'].get('remote_addr'):
ip_address = ipaddress.ip_address(
unicode(context['_request']['remote_addr']))
prefix = config.getint(
'session', 'ip_network_%s' % ip_address.version)
ip_network = ipaddress.ip_network(
unicode(context['_request']['remote_addr']))
ip_network = ip_network.supernet(new_prefix=prefix)
return ip_address, ip_network
def _login_size(func):
@wraps(func)
def wrapper(cls, login, *args, **kwargs):
@@ -662,7 +685,12 @@ class LoginAttempt(ModelSQL):
table = cls.__table__()
cursor.execute(*table.delete(where=table.create_date < cls.delay()))
cls.create([{'login': login}])
ip_address, ip_network = cls.ipaddress()
cls.create([{
'login': login,
'ip_address': str(ip_address),
'ip_network': str(ip_network),
}])
@classmethod
@_login_size
@@ -676,11 +704,21 @@ class LoginAttempt(ModelSQL):
def count(cls, login):
cursor = Transaction().connection.cursor()
table = cls.__table__()
cursor.execute(*table.select(Count(Literal(1)),
cursor.execute(*table.select(Count(Literal('*')),
where=(table.login == login)
& (table.create_date >= cls.delay())))
return cursor.fetchone()[0]
@classmethod
def count_ip(cls):
cursor = Transaction().connection.cursor()
table = cls.__table__()
_, ip_network = cls.ipaddress()
cursor.execute(*table.select(Count(Literal('*')),
where=(table.ip_network == str(ip_network))
& (table.create_date >= cls.delay())))
return cursor.fetchone()[0]
del _login_size