A good software development framework should make the common things easy and make the uncommon things possible.
Unfortunately, Django sometimes makes the simple things easy and the hard things possible — and security is hard!
The Django community does take security very seriously.
The ORM makes it really difficult to expose your app to SQL injection attacks. The template processing system makes it hard to enable cross-site scripting. It takes work to avoid Django's CSRF protection, and it'd be rare to subvert its well-tested session handling.
Not only that, but Django's documentation and release notes go the extra mile, discouraging many poor practices and even warning against problems outside of Django that could affect the security of a web app.
So what's the problem?
Django has its roots in the publishing industry and got its wings as a basis for sharing-oriented "Web 2.0" sites. When a majority of resources are publicly available, or shared among all logged-in users, it's possible to focus on securing a few private corners.
What Django's design considers uncommon is "multitenant" apps — imagine that instead of adding a blog to your company website, you are building a corporate blog–hosting service.
With a single-tenant app, there's generally some level of trust among all users. Maybe an intern is only supposed to edit customer support documents, but discovers a bug in the custom CMS built on Django that lets him post a funny picture of his boss on the homepage. Sure it was technically the Django-using programmer who let it happen, but it was the intern who betrayed the tenant's trust.
With multiple tenants, the responsibility of trust is upon the developers. When some computer-savvy ACME Corp employees find a hole that lets them access Wonder Inc's draft blog posts, they're just doing their job. If Wonder Inc imagined their exciting product announcements were safe inside your Django app, they won't care how easy it was to make that security mistake.
Most days, most developers are struggling valiantly just to get their code to work. Getting it to "compile", getting it to "run", getting it to run "on the production database". Fixing it to stay running even when a user clicks Y before they click X.
Security is hard because you still have to do all the "just getting it to work", but you also have to make sure it doesn't work even if a different user clicks X, fakes Y and then does Z with a little help from A.
Let me make this clear: security mistakes are too common to be a problem of "stupid developers". Leave the PEBKAC mentality for the poor techs who have to support what they can't fix — we are developers and designers, busy developing our designs. Engineering, Enforcement and Education are wonderful, but usability is cheaper.
Django's ORM makes it easy — too easy — to expose database rows to users who shouldn't have access. It provides a very user-friendly mapping from SQL to model objects. The catch is, the database doesn't give a rat's rooty-tooty about your app's permission model, and neither does the ORM. Its job is to be the floppy disk for your spreadsheets, the ORM's job is to pretend the spreadsheet rows are documents. Fair enough. But the tools Django provides for validating data access are too difficult to customize for an app where every table is shared among mutually untrusted tenants. Remember that developers are naturally inclined to code until it works for them — not to prove that the same code won't work when an attacker calls it up.
The template processing and file handling infrastructure encourage developers to expose private user uploads via statically hosted media directories. This is fine for a blog, but when a user notices their private upload got renamed to "/media/user_images/image___.jpg" they might start figuring out that Apache will gladly let them see "image___.jpg" (and "image.php"!) in that directory too.
Finally, while most of Django's middleware does enhance web app security, the error debugging system can lead to inadvertent storage of sensitive user data if an exception catches it mid-flight. This issue is being addressed for Django 1.4, although the design is opt-in and may be a bit fragile in practice — but this particular problem is both hard and uncommon. In this last case I suspect the solution being built in is a good enough design.
That leaves us with Django's ORM and file handling — which I'm convinced are not good enough designs for a multitenant web app framework.
In a multitenant app it is very common that model lookups and form validation must be contained to a stricter subset of data than Django encourages.
The very best solution to this problem is to partition your app. Give each tenant their own virtual system, their own database — in short, their own copy of the hosted app.Partitioning does take more work to configure up-front, but that's the best place for investments like that. It also complicates cross-account administration features: which is exactly the point. Make the uncommon use cases the harder ones, so that the normal stuff is securer by default.
If you're not ready or it's too late to partition, do your whole team a favor and stop using Django's ORM and ModelForms directly in a multitenant codebase. You need to write an API and force all your code to use it, instead of the ORM. Django's views are too presentation-focused. Not the place to expect secure code. When coding up a working user interface, it's too easy to say "My code needs this object!" when you mean "Some user would like to access this data?". Give day-to-day development the freedom to wholeheartedly fight For the user. Build an internal Python data access API for the sole purpose of standing between the user request and the ORM or filesystem; a good gate on this border can keep a thousand welcome mats safe.
Whether you partition your app into single-tenant instances or use an API to isolate data access, you should develop tests primarily for security. If a commit breaks functionality, it's an obvious bug. Someone will complain soon enough. If a code change only adds "functionality" that isn't supposed to exist, it's a zero-day. Will you notice the mistake in time?
Interestingly enough, our security tests do tend to catch functionality regressions too, since they really must check that Mallory can't do something Alice and Bob can.That's a nice benefit, especially since you're still updating tests because the app is getting better and its security needs to as well. (Having to maintain tests that only lock in functionality as it continually changes, sucks.)
Focus your programmatic testing efforts on permissions enforcement. Your time is precious — don't bother with automated tests for anything less valuable than earning trust!
Django is a great traditional web framework that makes many customizations easy. It's possible to build secure multitenant apps using the pieces Django provides, although certain built-in features and certain patterns encouraged by the documentation need to be avoided.
I suspect this is also the case with many other web frameworks. And security might not be the only area where developers' toolkits make doing things "the wrong way" the easy way.
Pay attention to design decisions at the framework level that distract your team from delivering a great user experience at a higher level.
Avoid shooting yourselves in the foot (feets?) by only picking fights on fronts where the troops will stay engaged. Make solving interesting problems the only uphill battle for your developers. Then level the field for your customers. (That's what usability is about.)
To see us work it's not self-evident, but nerds invented computers to avoid tedious mistake-prone work. Like end-users, developers have lives and are busy and are experts only in their own passions. Assume security will be taken for granted by users, and developers alike!
If secure web apps should be common, vulnerable code must be made hard to write. It is a good workman's responsibility to blame his tools every now and then — occasionally we get something as useful as Django as a result!