Is your Django settings.py being served as source? SECRET_KEY signs every session you issue.
A Django project keeps its keys to the kingdom in one file: settings.py. The database host, name, user, and password live there under DATABASES. The SECRET_KEY lives there too, and that one value signs session cookies, CSRF tokens, password-reset links, and signed cookies across the whole app. Normally none of this is reachable, because settings.py is Python that the server executes, not text it serves. The browser never sees the source.
The exception is when something points the web root at the project directory and the server hands .py files back as plain text instead of running them. Now GET /settings.py returns the source, and everything in it is readable: the credentials to your database and the secret that signs your users' sessions. The app keeps working perfectly while its single most sensitive file is a download. It's a configuration accident, not a code bug, which is exactly why it survives in production unnoticed.
What the source hands over
Two things in settings.py matter most, and they fail in different directions.
The DATABASES block is the obvious one: host, user, and password to your production database, often reachable if the database isn't firewalled tightly. The SECRET_KEY is the subtler and arguably worse one. Django uses it to sign session data, so an attacker holding it can forge a session cookie that Django accepts as genuine, logging in as any user without a password. It also signs password-reset tokens and the default signing for django.core.signing, so the leak reaches into account recovery and any signed value your app trusts. One string, and your session integrity is gone.
Why a .py file gets served as text
Django's own dev server never does this, which is part of why it's surprising in production. The cause is always in the layer in front.
The classic version is a misconfigured docroot: the web server (nginx, Apache) is pointed at the project root, and there's no rule routing .py requests to the WSGI application, so the static-file handler picks them up and serves them verbatim. Another version is a project laid out so the source sits inside a directory the server treats as static, staticfiles collected into a path that overlaps the code, for instance. A third is a deploy that copies the whole repo into a served location and relies on "nobody will request settings.py," which holds right up until a scanner does. In every case Django is fine; the web server in front of it is handing out source it should be executing or hiding.
Move the docroot, then rotate the key
The fix is to make sure source files are never served, and to treat a leaked SECRET_KEY as a key that must be replaced.
# nginx pointed at the project root, no WSGI route for .py
root /srv/myproject; # settings.py is now reachable
location / { try_files $uri $uri/ =404; }Point the web server at only the static and media directories on disk, and route everything else through the WSGI socket, so a request for settings.py has no path to a raw file. Move secrets out of settings.py into environment variables (os.environ) so the file holds references, not values, and even a served copy gives up less. And if the file was ever reachable, rotate SECRET_KEY immediately: every session cookie and reset token signed with the old key is forgeable until you do, and replacing it invalidates them, which is the point. Rotate the database password in the same breath.
This is visible from outside because a served settings.py is unmistakable Python: a real SECRET_KEY assignment with a value, or the DATABASES / INSTALLED_APPS structure Django uses nowhere else. SurfaceCheckr requests the path and flags it only when that signature is present with a genuine value, not on a 404 or a soft-error page, and it reads enough of the head of the file to confirm the leak without pulling the whole thing down. For the same accident in other stacks, a Rails database.yml or secrets.yml served raw is the parallel finding, and the deeper pattern, a session-signing secret a stranger can read, is what makes the SECRET_KEY half of this so dangerous.
Read next
- Is your .git folder public? Strangers can rebuild your source from itThe files you forgot you deployed
- A public .svn folder rebuilds your source the same way a .git one doesThe files you forgot you deployed
- Your Ansible inventory lists every server you run, and the passwords to log into themThe files you forgot you deployed
- The error log and access log in your web root are a replay of every request, tokens includedThe files you forgot you deployed
Find it before someone else does.
Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.