Tracking/logging bruteforcing, especially on admin accounts?

194 views
Skip to first unread message

Vaibhav Mallya

unread,
May 19, 2016, 7:11:27 AM5/19/16
to Django developers (Contributions to Django itself)
Hi everyone,

I've been having a great chat with @jacobian here about potential security improvements to the Django admin UI: https://gist.github.com/mallyvai/bcb0bb827d6d53212879dff23cf15d03

The admin UI is core to Django's value-prop, and it seems undersecured by modern standards. I wanted to raise the possibility on this list of guarding against brute-force login attempts on admin user accounts.

You could imagine tracking / throttle in two main ways.


1. By originating IP
2. By number of login attempts on a user account


In my view, #2 would be the best starting point - there are going to be a relatively small number of admin accounts on the average Django site. Ergo, focused brute-forcing or spearfishing seems to be a greater threat than getting into an admin account via a lot of scanning.

I am proposing a solution broken into two parts - tracking and enforcing.

Tracking - End goal would be logging SuspiciousOperation if appropriate thresholds were crossed. We’d need to store server-side state. I don’t believe we don’t have a clean heap data structure across all DBs that the Django ORM supports, but we could, say, keep two additional columns on each user object: last_login_attempt_window_start, and num_login_attepts_on_window_start, and checking / updating both on any / all login attempts. Alternatively, simply serializing a Python heap-list for each user may work.

Or we can simply leverage the cache backend to store state.

Enforcing - Rejecting login attempts [on any basis] is probably not a good idea for a default - we can’t guarantee we don’t introduce some other DoS-style attack vector. But there are some NIST/etc guidelines around, say, forcing pauses between login attempts, exponential backoff, forcing email-distributed tokens to be used, etc.

We’re already storing custom auth/session information for the Django user model, so storing state/migrations/etc somewhere wouldn’t be too much of a departure.

Thanks!
-Vaibhav

Cristiano Coelho

unread,
May 19, 2016, 7:40:33 AM5/19/16
to Django developers (Contributions to Django itself)
IP based throttling like django-rest-framework would be ideal! I know there are some 3rd party libraries that tries to add ip based throttling to django although not as cool as drf ones.

Aymeric Augustin

unread,
May 19, 2016, 7:57:50 AM5/19/16
to django-d...@googlegroups.com
Hello Vaibhav,

On 19 May 2016, at 08:59, Vaibhav Mallya <vaibha...@gmail.com> wrote:

In my view, #2 would be the best starting point - there are going to be a relatively small number of admin accounts on the average Django site. Ergo, focused brute-forcing or spearfishing seems to be a greater threat than getting into an admin account via a lot of scanning.

This makes sense for django.contrib.admin. However I suggest to consider this feature as an enhancement of django.contrib.auth and consider a more general use case. That seems significantly more useful — assuming the generalization doesn’t make the problem intractable.

You should look at prior art such as django-ratelimit-backend.

I am proposing a solution broken into two parts - tracking and enforcing.

Tracking - End goal would be logging SuspiciousOperation if appropriate thresholds were crossed. We’d need to store server-side state. I don’t believe we don’t have a clean heap data structure across all DBs that the Django ORM supports, but we could, say, keep two additional columns on each user object: last_login_attempt_window_start, and num_login_attepts_on_window_start, and checking / updating both on any / all login attempts. Alternatively, simply serializing a Python heap-list for each user may work.

Or we can simply leverage the cache backend to store state.

Please use the cache.

Django’s “last_login” field in the user model is a common cause of performance issues in large sites. (I believe it’s required to secure password resets so we can’t remove it.) Let’s avoid adding more fields with similar behavior. (The PostgreSQL experts subscribed to this mailing list should be able to confirm that part.)

Enforcing - Rejecting login attempts [on any basis] is probably not a good idea for a default - we can’t guarantee we don’t introduce some other DoS-style attack vector. But there are some NIST/etc guidelines around, say, forcing pauses between login attempts, exponential backoff, forcing email-distributed tokens to be used, etc.

We’re already storing custom auth/session information for the Django user model, so storing state/migrations/etc somewhere wouldn’t be too much of a departure.

This is an interesting part of the problem. If it has been explored by current third-party solutions, then I’m not aware of it. Django should provide a reasonable default policy. Besides it would be nice to support selecting another policy. Such configurability would be consistent with how Django handles situations where various behaviors are acceptable.

-- 
Aymeric.

Florian Apolloner

unread,
May 19, 2016, 8:34:41 AM5/19/16
to Django developers (Contributions to Django itself)


On Thursday, May 19, 2016 at 1:57:50 PM UTC+2, Aymeric Augustin wrote:
Django’s “last_login” field in the user model is a common cause of performance issues in large sites. (I believe it’s required to secure password resets so we can’t remove it.) Let’s avoid adding more fields with similar behavior. (The PostgreSQL experts subscribed to this mailing list should be able to confirm that part.)

Not a postgres expert -- but an "often changing" field in a row certainly bloats the table. It would be nice if it were in an extra table, so rewriting the row does not include all the data like username and email (though I do realize this its probably not an option).

As for any changes to the admin itself, I am somewhat -0 to -1 on them, unless it can be clearly shown that we cannot do this via decorators etc in a generic way (there are probably plenty of views out there which could benefit from ratelimiting).

As for the rest of the proposed changes (see the gist):
 * Let's encrypt -- this should not be done by Django
 * 2FA: Yes, I'd like to see at least U2F and TOTP support ootb.
 
When it comes to tracking/rate limiting: No new fields for the models please, this should all be in cache (this is absolutely information you do not want to persist and also changes rapidly during attacks…)

Cheers,
Florian

Josh Smeaton

unread,
May 19, 2016, 8:44:41 PM5/19/16
to Django developers (Contributions to Django itself)
I understand the reasoning for "use the cache", but not every site has caching enabled, especially lots of smaller sites. A separate table could be used for tracking attempts, and cleared out per user on successful login attempt/ip address. This table would not need to be huge if designed with performance in mind. Caching could be layered on (check cache for bans but not for maintaining counts) or we could have a cache backend in addition to a database backend. I'm aware that django has a database cache backend, but any database solution in this space should be designed with the database in mind and not just essentially blob storage.

For what it's worth, I've used django-axes (https://pypi.python.org/pypi/django-axes) with some success in the past and will probably use it again for my current project. I don't think the username based protection is adequate though:

AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP: If True prevents to login from IP under particular user if attempts limit exceed, otherwise lock out based on IP.

Distributed attack on a particular user would not be caught in this configuration. 

Cheers

Claude Paroz

unread,
May 20, 2016, 2:36:14 AM5/20/16
to Django developers (Contributions to Django itself)
Le vendredi 20 mai 2016 02:44:41 UTC+2, Josh Smeaton a écrit :
I understand the reasoning for "use the cache", but not every site has caching enabled, especially lots of smaller sites.

That's not true. When not specified, the cache backend default to the local memory cache:
https://docs.djangoproject.com/en/1.9/topics/cache/#local-memory-caching

It's per-process, so not optimal, but still, Django can count on a default cache.

Claude
Reply all
Reply to author
Forward
0 new messages