The .well-known/security.txt File That 80% of Projects Are Missing
The .well-known/security.txt File That 80% of Projects Are Missing
Of the 30 open-source projects in our Security Headers benchmark, 21 do not have a security.txt file. That is 70% — and the real number across all web projects is almost certainly higher.
The check is security-headers.basic-hygiene.security-txt. It is weighted as "info" severity, which means it contributes minimally to the overall score. But its practical importance is inversely proportional to its weight. A missing security.txt does not make your app insecure. It makes your app harder to secure when someone finds a vulnerability.
What security.txt is
RFC 9116 (published 2022, replacing the earlier draft from 2017) defines a standard file at /.well-known/security.txt that tells security researchers how to report vulnerabilities in your project. That is it. It is a plain text file with contact information.
Before this standard existed, a researcher who found a vulnerability in your app had to guess: Do I email info@? Do I file a GitHub issue (publicly disclosing the vulnerability)? Do I DM someone on Twitter? Do I just publish it?
Many researchers give up. Some publish. Neither outcome is good for you.
Who has it, who does not
Projects with security.txt (9 of 30):
- Documenso
- Dub
- Midday
- Supabase
- Plane
- Cal.com
- Infisical
- Saleor
- Medusa
Projects without security.txt (21 of 30): The other 21, including well-known projects like Hoppscotch, Formbricks, Appsmith, PostHog, Novu, Strapi, Directus, Typebot, and many more.
Some of these projects have a SECURITY.md file in their GitHub repository — which is good practice for open-source projects. But SECURITY.md lives in your repo. security.txt lives at a well-known URL on your deployed application. A researcher scanning your production site will find the latter. They will not check your GitHub.
Why it matters more than its weight suggests
The security.txt check is weighted as "info" severity (weight: 1, compared to 10 for critical and 3 for high/medium). That weighting is correct from a scoring perspective: missing security.txt does not directly create a vulnerability. But consider the failure mode:
- Researcher finds XSS vulnerability in your app
- Researcher looks for
/.well-known/security.txt— not found - Researcher checks for a
SECURITY.md— maybe finds it, maybe does not - Researcher emails a generic contact address — maybe monitored, maybe not
- Researcher waits 90 days per responsible disclosure norms
- Researcher publishes
Now compare:
- Researcher finds XSS vulnerability in your app
- Researcher hits
/.well-known/security.txt— findssecurity@yourcompany.comand your PGP key - Researcher sends encrypted disclosure to a monitored inbox
- You fix it before anyone else knows
The difference between these two paths is a 5-minute file.
How to add security.txt
The file
Create /.well-known/security.txt (or /security.txt — the spec supports both, but .well-known is preferred):
Contact: mailto:security@yourcompany.com
Expires: 2027-04-01T00:00:00.000Z
Preferred-Languages: en
Canonical: https://yourapp.com/.well-known/security.txt
Policy: https://yourapp.com/security-policy
Required fields per RFC 9116:
- Contact — must be a URI (mailto:, https://, or tel:). This is the only truly required field.
- Expires — an expiration date so stale files get noticed. Required as of RFC 9116.
Optional but recommended:
- Preferred-Languages — language(s) you can handle reports in
- Canonical — the authoritative URL for this file (helps prevent spoofing)
- Policy — link to your vulnerability disclosure policy
- Encryption — link to your PGP key for encrypted reports
- Acknowledgments — link to a page thanking past reporters (incentivizes future reports)
- Hiring — link to security job openings (researchers read these)
Next.js (App Router)
// app/.well-known/security.txt/route.ts
export async function GET() {
const securityTxt = `Contact: mailto:security@yourcompany.com
Expires: 2027-04-01T00:00:00.000Z
Preferred-Languages: en
Canonical: https://yourapp.com/.well-known/security.txt
Policy: https://yourapp.com/security-policy`;
return new Response(securityTxt, {
headers: { 'Content-Type': 'text/plain' },
});
}
Static site (public directory)
Drop the file at public/.well-known/security.txt. Done.
Express
app.get('/.well-known/security.txt', (req, res) => {
res.type('text/plain').send(`Contact: mailto:security@yourcompany.com
Expires: 2027-04-01T00:00:00.000Z
Preferred-Languages: en`);
});
Nginx
location = /.well-known/security.txt {
alias /etc/nginx/security.txt;
default_type text/plain;
}
Common mistakes
Setting Expires too far in the future. The point of Expires is to force you to revisit the file periodically. Set it 1 year out, then update it annually. If a researcher finds an expired security.txt, they may assume the contact is no longer monitored.
Using a personal email. Use a role-based address like security@ that routes to your security team or at least a monitored inbox. People leave companies. Role addresses do not.
Forgetting to actually monitor the inbox. This defeats the entire purpose. If security@yourcompany.com goes to a black hole, you have a security.txt that creates false confidence. Set up alerts.
Not having a vulnerability disclosure policy. The Policy field should link to a page explaining what you consider in-scope, what your response timeline is, and whether you offer a bounty. Even a simple "we will acknowledge within 48 hours and aim to fix critical issues within 30 days" is better than nothing.
It is 5 minutes
Adding security.txt is one of the highest-value, lowest-effort security improvements you can make. It does not prevent attacks. It does not harden your application. What it does is ensure that when someone finds a problem, they can tell you about it privately before it becomes a public incident.
Twenty-one of the thirty projects in our benchmark are missing it. Do not be the twenty-second.