Skip to content

Instantly share code, notes, and snippets.

@mallyvai
Last active May 19, 2016 06:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mallyvai/bcb0bb827d6d53212879dff23cf15d03 to your computer and use it in GitHub Desktop.
Save mallyvai/bcb0bb827d6d53212879dff23cf15d03 to your computer and use it in GitHub Desktop.
Thoughts on Django's admin UI
Django’s admin UI is wonderful, and a big part of the reason why we chose it for OfferLetter.io. Unfortunately the admin panel itself is extremely insecure, with simple password authentication and no modern security infrastructure.
Given how widely-used and critical this admin UI is, I think that adding better, modern security practices would go a long way to keeping Django safe. Some rough ideas are below, in rough order of ease of implementation.
> Logging CSRF exceptions as special SuspiciousOperation security exceptions. CSRF tokens/cookies exist to provide security, and if there is some rogue actor, I’d like to know about as soon as possible. This is easy now to do now using the 403 handler. But in my view, the sensible default here is to treat it as a security violation and SuspiciousOperation.
> Logging failed admin account login attempts as SuspiciousOperation (including the IP). This will be highly-leveraged - easy to implement and straightforward.
> First-class recaptcha integration. ReCaptcha is public and free, and widely-used and well-supported. One wouldn’t have to store modify tables or store any state in the database. As a downside, it would introduce an important dependency on an external / third-party service. If the service was down, or the API changed, that would have to be accounted for quickly.
> Enforcing strong passwords for admin users by default. We force OfferLetter.io to use 1Password/LastPass, but there really should be a better sensible default here here to make sure people don’t accidentally do something really dumb - either making the user confirm they want a weak password, or rejecting weak passwords outright.
There’s also a race condition of sorts here - That is if a normal user is created at first, then has the admin bit set. One potential solution is to enforce the same standard for all users, not just admin.
> Throttling login attempts. You could imagine tracking / throttle in two main ways.
By originating IP in last N minutes
By number of login attempts on a user account in the last N minutes.
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 solution is broken into two parts - tracking and enforcing.
Tracking - 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_attempts_on_window_start, and checking / updating both on any / all login attempts. Alternatively, simply serializing a Python heap-list for each user may work..
Enforcing - As with any failure mode, we can start by logging and then by ramping up strictness. Rejecting is probably not a good idea for a default - we can’t guarantee we don’t introduce some other DoS-style attack.
We’re already storing custom auth/session information for the Django user model, so storing state/migrations/etc here wouldn’t introduce anything too crazy.
> 2FA auth integration. There are Python / Django integrations that provide it, but it would go a long way to have first-class integration support in the framework. It would be time-consuming to add and support. It would also be incredibly valuable, and keep Django sites by default in line with Google, Dropbox, etc.
> LetsEncrypt integration. This would be incredibly hard, but LetsEncrypt is the new standard for HTTPS - it’s free! and providing first-class support such that one could push a button and update/revoke/etc certs would be grand. We (OfferLetter.io) have a poor man’s version of this right now to simplify the auth/reauth flow for manual cert deployment. Integrating this into Django would be hard, and probably not a great idea now, especially since LetsEncrypt itself is still rapidly evolving. But - it would be pretty damn cool! ^_^
While managing scope and creep is always challenging, one or two key improvements would meaningfully improve the security here. As a bonus, many of the features would be easily transferable to all user accounts, improving user confidence in general.
-Vaibhav
@jacobian
Copy link

This is great, thank you! Some notes/thoughts:

Logging CSRF exceptions as special SuspiciousOperation security exceptions.

Agreed; filed as https://code.djangoproject.com/ticket/26628.

Logging failed admin account login attempts as SuspiciousOperation (including the IP).

Agreed: https://code.djangoproject.com/ticket/26629.

First-class recaptcha integration.

I think this is a better candidate for a third-party app. (I've used https://github.com/praekelt/django-recaptcha in the past with good success). For one, it shouldn't require deep hooks into Django, so there's no great reason to include it out of the box. And, CAPTCHA use is controversial, and we tend to want to focus on the things that are clear best practices.

That said, if you wanted to drive this forward, a good next step would be to take the idea to the django-developers mailing list and see if there's a rough consensus that it's a good idea.

Enforcing strong passwords for admin users by default.

As of 1.9 this built-in: https://docs.djangoproject.com/en/1.9/topics/auth/passwords/#module-django.contrib.auth.password_validation.

Throttling login attempts.

I think I agree this shuold be built-in, but I'm not entirely sure. Again a third-party app might be a better choice; I've not used https://github.com/kencochrane/django-defender but it looks promising.

If we did want to make this a built-in, it'll be tricky to get right for a number of reasons:

  • Different people are going to have rather different requirements, so there's a need to be configurable.
  • Login lockouts can be a DoS vector, so the defaults matter (a lot) - most people won't change the defaults, leaving most Django sites vulnerable to login DoS.
  • As you note, the question of where to store state is tricky. django-defender uses Redis, which is a great choice, but not a good default since it requires another moving piece. So, there'd need to be some sort of "pluggable backend" concept (or, maybe, use the cache engine?)

This is another place that, if you want to drive it forward, would be good to start a thread on -developers about.

2FA auth integration.

http://django-two-factor-auth.readthedocs.io/en/stable/ does this very well right now as a external app. I would love to look into integrating this into Django 1.11; I think lowering the bar to adding 2FA support would be good for Django. I plan on working on this in the coming months.

LetsEncrypt integration.

I'm not sure what this would mean, since Django isn't a web server. This is the kind of thing that would be better off pushed into things like gunicorn, waitress, nginx, etc.

Thanks again for the feedback, this is a great list!

@claudep
Copy link

claudep commented May 17, 2016

@mallyvai
Copy link
Author

mallyvai commented May 19, 2016

And, CAPTCHA use is controversial, and we tend to want to focus on the things that are clear best practices.

This is interesting, I wasn't aware there was controversy in the sec community around it. Would love some links or resources on discussions? If it's not a clear best-practice, doesn't make sense to pursue IMO.

http://django-two-factor-auth.readthedocs.io/en/stable/

This looks great! I wasn't aware support was that good yet. Definitely will put integrating this on our roadmap.

Throttling login attempts

The cache backend seems like a good bet to me to store the state.

Definitely agreed about DoS vector being a problem, and sensible-but-configurable-defaults being important. One random thought - one could conceivably kick off an instant 2FA flow of sorts if someone was trying to log into their account the same time it was trying to be brute-forced. e.g:

  • Your account starts being brute-forced
  • You automatically get an email with a special URL with a nonce in it for the admin page
  • For the duration of the bruteforce attempt you can't login unless you are using this special URL to login

Of course this is complicated and needs to be thought through, and I don't know if something this comprehensive is in-scope for the framework. But IIRC there are some sites that force knowledge-based questions and other random things.

Even if there is never real throttling for login attempts, I think it's a good idea to at least track and alert SuspiciousOperation if some bruteforcing was happening, especially for admin accounts. I'll poke the appropriate mailing list.

LetsEncrypt

The --manual LetsEncrypt domain (re)verification flow needs you to 'do stuff' at the web app level on some setups [Heroku in our case]. Roughly:

  1. Run a command locally with appropriate creds
  2. It generates two long strings
  3. One corresponds to a URL [U] and the other to the value [V] expected to be returned from that URL
  4. You make sure your application can respond with the value [V] from the URL [U]

I created a simple Django app that lets us update both [U] and [V] via the admin UI, instead of having to do a full deploy. More generally, using the Django UI to provide LetsEncrypt functionality seems cool. But again, more of a pipe dream and way, way out of scope for the core Django framework ^_^

Thanks for taking the time to seriously consider these!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment