DEV Community

Cover image for Why Most Django Boilerplates Are Insecure by Default
siyadhkc
siyadhkc

Posted on

Why Most Django Boilerplates Are Insecure by Default

You found a Django boilerplate on GitHub. It has 2k stars, a clean README, Docker support, and a Makefile. You clone it, rename a few things, push to production.

Congratulations. You just shipped someone else's security assumptions.


The Illusion of "Production-Ready"

Every boilerplate claims to be production-ready. The badge is right there in the README. But production-ready for what? A side project? A fintech API? A platform that stores user PII?

"Production-ready" in most boilerplates means: it runs without errors. That's it. The security part is usually left as a comment that says # TODO: change this before deploy — which, statistically, nobody changes.

This isn't an attack on boilerplate authors. They're solving a real problem: Django's initial setup is tedious, and abstracting that away has genuine value. The problem is that the defaults they choose get inherited silently by every project that forks them.

And defaults are everything in security.


What Actually Ships in Most Boilerplates

Let me walk through what you'll typically find if you dig past the README.

1. DEBUG = True Tied to an Environment Variable That Defaults to True

# settings.py
DEBUG = os.environ.get("DEBUG", "True") == "True"
Enter fullscreen mode Exit fullscreen mode

This is the canonical mistake. The intent is good — make DEBUG configurable. But the default is True. So if you deploy and forget to set the environment variable, Django will cheerfully expose your full stack trace, local variables, installed apps, and settings to anyone who hits a 404.

I've seen this in repos with thousands of stars. The author probably never intended it to ship this way. But it does.

The fix is trivial:

DEBUG = os.environ.get("DEBUG", "False") == "True"
Enter fullscreen mode Exit fullscreen mode

One word. The default should always be the safe option.


2. SECRET_KEY Hardcoded or Weakly Generated

SECRET_KEY = "django-insecure-abc123xyz-changeme"
Enter fullscreen mode Exit fullscreen mode

Django's startproject actually warns you with the django-insecure- prefix now. But plenty of boilerplates generated before that convention still have static keys checked into version control.

Even the ones that generate it dynamically sometimes do it wrong:

# generates a NEW key on every restart
SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key())
Enter fullscreen mode Exit fullscreen mode

This means every server restart invalidates all existing sessions and CSRF tokens. That's not a security fix — that's a different bug wearing a security costume.

The secret key needs to be generated once, stored as an environment variable, and never committed to Git. Full stop.


3. ALLOWED_HOSTS = ["*"]

ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "*").split(",")
Enter fullscreen mode Exit fullscreen mode

Again — the default is the dangerous value. ALLOWED_HOSTS = ["*"] disables Django's Host header validation entirely, opening the door for host header injection attacks. This isn't theoretical. Attackers use it to poison password reset emails with their own domain.

The boilerplate author probably put "*" there so you can docker-compose up without configuring anything. Convenience for the developer. Risk for the user.


4. CORS Configured Wide Open

# django-cors-headers
CORS_ALLOW_ALL_ORIGINS = True
Enter fullscreen mode Exit fullscreen mode

CORS exists to protect your users' browsers. When you set this to True, you're telling every origin on the internet that it can make credentialed requests to your API. Combined with SESSION_COOKIE_SAMESITE = None (which some boilerplates also set), you've recreated the conditions for CSRF attacks in a Single Page App context.

The reasoning is always the same: "I'll change this when I know my frontend's domain." But that moment never comes cleanly in the chaos of early development, and it ships.


5. No Security Headers

Django doesn't set security headers by default. You need django.middleware.security.SecurityMiddleware in MIDDLEWARE, and then you need to actually configure it:

SECURE_BROWSER_XSS_FILTER = True  # deprecated but harmless
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_SSL_REDIRECT = True
Enter fullscreen mode Exit fullscreen mode

Most boilerplates have SecurityMiddleware in the list. Few have these settings configured. HSTS in particular is almost always missing — which means every user's first request is over HTTP until they get redirected.


6. Session and Cookie Settings Left at Defaults

# What most boilerplates ship
SESSION_COOKIE_HTTPONLY = True   # ✅ Django default, usually fine
SESSION_COOKIE_SECURE = False    # ❌ Default — cookies sent over HTTP
CSRF_COOKIE_SECURE = False       # ❌ Default — CSRF token sent over HTTP
SESSION_COOKIE_SAMESITE = "Lax"  # ✅ Usually fine, but often overridden
Enter fullscreen mode Exit fullscreen mode

SESSION_COOKIE_SECURE = True means the browser will only send the session cookie over HTTPS. Leaving it False means someone on the same network can intercept it over HTTP. This is a solved problem — but it requires explicitly setting it, and boilerplates rarely do.


7. Django Admin Sitting on /admin/

urlpatterns = [
    path("admin/", admin.site.urls),
    ...
]
Enter fullscreen mode Exit fullscreen mode

This is Django's own default, so I can't blame boilerplate authors entirely. But nobody changes it either. The admin panel at /admin/ is the first thing automated scanners probe. Moving it to something non-guessable costs you nothing and removes you from a significant percentage of automated attacks.

path("your-own-path-here/", admin.site.urls),
Enter fullscreen mode Exit fullscreen mode

Why This Keeps Happening

The ecosystem isn't malicious. It's optimizing for the wrong metric.

Boilerplates get stars based on how quickly they get you from zero to running. Security settings that would break a local setup — like SECURE_SSL_REDIRECT = True without an SSL cert — get disabled so the DX is smooth. The secure version is the harder version to demo.

Django itself deserves some of this too. The framework ships with safe defaults in some areas (CSRF protection is on by default, passwords are hashed), but makes you opt-in to the rest. That opt-in surface is where boilerplates consistently fail to follow through.

And then there's us — developers who clone first, configure later, and ship before "later" arrives.


What a Secure Boilerplate Default Looks Like

Split your settings. This is non-negotiable.

settings/
    base.py      # shared across all environments
    local.py     # DEBUG=True, no SSL, relaxed CORS
    production.py  # strict everything
Enter fullscreen mode Exit fullscreen mode

In production.py, the defaults should be secure. If something is missing — a SECRET_KEY, a DATABASE_URL — the app should refuse to start, not silently fall back to an insecure value.

# production.py
SECRET_KEY = os.environ["SECRET_KEY"]  # KeyError if missing — intentional
DEBUG = False
ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(",")

SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CORS_ALLOWED_ORIGINS = os.environ["CORS_ALLOWED_ORIGINS"].split(",")
Enter fullscreen mode Exit fullscreen mode

Notice the os.environ["KEY"] without .get(). If the variable isn't set, it raises a KeyError and the app doesn't start. That's correct behavior. Failing loudly in production is safer than running silently insecure.


A Checklist Before You Ship

If you're using any boilerplate — including one you wrote yourself six months ago — run through this before you deploy:

  • [ ] DEBUG = False in production, with no unsafe default fallback
  • [ ] SECRET_KEY sourced from environment, never in version control
  • [ ] ALLOWED_HOSTS explicitly set, no wildcards
  • [ ] CORS_ALLOW_ALL_ORIGINS = False, origins explicitly whitelisted
  • [ ] SESSION_COOKIE_SECURE = True
  • [ ] CSRF_COOKIE_SECURE = True
  • [ ] SECURE_SSL_REDIRECT = True
  • [ ] SECURE_HSTS_SECONDS set
  • [ ] Django admin URL is not /admin/
  • [ ] django.middleware.security.SecurityMiddleware is first in MIDDLEWARE

Run python manage.py check --deploy too. Django ships a deployment checker that will call out many of these issues. Most developers don't know it exists.


The Honest Takeaway

Boilerplates are a starting point, not a finished product. The problem is the word "boilerplate" implies it's something you don't need to think about — just add your code on top. But the security layer is exactly the part that requires thinking.

The ecosystem needs better defaults. Boilerplate authors should make the safe behavior the path of least resistance, not the other way around. Django could be more opinionated about production configuration. And we — as developers shipping things to real users — need to stop treating security settings as a post-launch concern.

It never becomes post-launch. It becomes a breach report.


If you found this useful, I write about Django security, API security, and backend architecture on Dev.to. The next piece will be on OWASP API Top 10 vulnerabilities in real Django REST Framework code — the ones that slip through code review.

Top comments (0)