Add security.txt and a Vulnerability Disclosure Process to a Django Site
Publish /.well-known/security.txt from Django with contact, expiry, policy, and encryption fields, set up a vulnerability disclosure policy and intake mailbox, triage reports, and avoid common mistakes like expired files and unmonitored addresses.
Problem statement
A researcher finds a vulnerability in your Django app and wants to tell you. There is no obvious contact, so they write to a generic support address that auto-closes tickets, post on social media, or give up. security.txt (RFC 9116) is a small text file at /.well-known/security.txt that tells people where and how to report security issues. It only helps if a monitored process sits behind it.
- Create a monitored address such as
security@example.comrouted to people who can triage. - Publish
/.well-known/security.txtwithContact,Expires,Policy, and optionallyEncryptionandPreferred-Languages. - Serve it from Django with a simple view (or from static files via Nginx or WhiteNoise) as
text/plain. - Write a short disclosure policy: scope, safe harbor, response times, and what not to do.
- Renew the
Expiresdate before it lapses, and test the contact path periodically.
Step-by-step solution
1. Set up intake
Use a dedicated address or form that creates tickets in a security queue, not general support. At least two people should receive it. Optionally publish a PGP key for encrypted reports. Define internal response targets: acknowledge within a few business days, triage within a week.
2. Write the file
Contact: mailto:security@example.com
Expires: 2027-10-01T00:00:00.000Z
Policy: https://example.com/security/policy/
Encryption: https://example.com/.well-known/pgp-key.txt
Preferred-Languages: en, de
Canonical: https://example.com/.well-known/security.txt
Contact and Expires are required by RFC 9116. Keep Expires less than a year in the future so the information stays fresh. Optionally sign the file with OpenPGP.
3. Serve it from Django
# core/views.py
from django.http import HttpResponse
from django.views.decorators.cache import cache_control
from django.views.decorators.http import require_GET
SECURITY_TXT = """Contact: mailto:security@example.com
Expires: 2027-10-01T00:00:00.000Z
Policy: https://example.com/security/policy/
Canonical: https://example.com/.well-known/security.txt
"""
@require_GET
@cache_control(max_age=86400, public=True)
def security_txt(request):
return HttpResponse(SECURITY_TXT, content_type="text/plain; charset=utf-8")
# urls.py
urlpatterns = [path(".well-known/security.txt", security_txt), ...]
Alternatively, render it from a template so Expires comes from settings, or serve a static file through Nginx. Make sure SECURE_SSL_REDIRECT and ALLOWED_HOSTS do not interfere, and that it is reachable on every public hostname.
4. Publish a disclosure policy
A short page covering:
- Scope: which domains and apps are in scope; third-party services are not.
- Safe harbor: you will not pursue legal action for good-faith research within the rules.
- Rules: no data exfiltration beyond proof, no denial of service, no social engineering, no access to other users' data.
- Process: how you acknowledge, triage, fix, and credit reporters, and expected timelines.
Whether to offer bounties is a separate decision; a clear policy without rewards still gets reports.
5. Triage and close the loop
Treat each report like an incident: reproduce, assess severity, fix, deploy, and inform the reporter. Track reports in a private issue tracker. Renew Expires on a calendar reminder and send a test email to the contact address quarterly.
Explanation
RFC 9116 standardizes a machine-readable file that tells researchers and automated tools how to report vulnerabilities. Its value depends entirely on the process behind it: a monitored contact, clear rules, and timely responses. For a Django site, serving the file is a few lines of code; maintaining the process is the real work, and it pays off by turning potential public disclosures into private reports you can fix first.
Edge cases / notes
- Multiple domains. Serve the file on every domain, or point
Canonicalto the main one. - Spam. Security mailboxes attract automated "beg bounty" reports; triage quickly and keep standard replies.
- Legal review. Have safe harbor language reviewed for your jurisdiction.
Next steps
- Harden responses with the Django Security Headers Checklist.
- Prepare for testing with the Django Penetration Test Preparation Checklist.
- Handle confirmed issues with the Django Incident Response Runbook.
FAQ
Where should security.txt be located?
At /.well-known/security.txt on each public domain, served over HTTPS as plain text.
Which fields are required?
Contact and Expires. Policy, Encryption, and Canonical are recommended.
Does security.txt replace a bug bounty program?
No. It points to your reporting process, which may or may not include rewards.
How often should I update it?
Before the Expires date, at least yearly, and whenever the contact or policy changes.