A vulnerability disclosure policy (VDP) is a public commitment that tells security researchers what they may test, how to report what they find, and what you promise in return: legal safe harbor, a response deadline, and a fix timeline. That is the whole job. Most VDPs fail because they were written by counsel to minimize liability rather than by engineers to maximize the odds that the next person who finds your SSRF actually tells you about it. I have been on both ends of this — reporting bugs into black holes and triaging reports at 2 a.m. — and the difference between a respected policy and an ignored one comes down to a handful of concrete choices.
Start with security.txt, not a lawyer
Before anyone reads your policy, they have to find it. RFC 9116 standardized the location: a plain text file at /.well-known/security.txt. It takes ten minutes to ship and most companies still get it wrong — the most common failure is a stale Expires field, which some scanners treat as "no valid policy."
A minimal file that passes validation:
Contact: mailto:security@example.com
Contact: https://github.com/example/example/security/advisories/new
Expires: 2025-04-22T11:00:00.000Z
Policy: https://example.com/security/disclosure
Preferred-Languages: en
Canonical: https://example.com/.well-known/security.txt
Two details worth stealing. First, list a GitHub private advisory URL as a second contact — many researchers prefer a structured form over composing an email. Second, set Expires twelve months out and put a calendar reminder to renew it. An expired security.txt reads as "we stopped caring in 2022."
Add a SECURITY.md at the root of every public repository too. GitHub surfaces it in the Security tab and in the "Report a vulnerability" flow, so it costs nothing and meets researchers where they already are.
Write scope like you mean it
Vague scope produces two failure modes: researchers skip you because they cannot tell what is fair game, or they test something you consider off-limits and you end up in a hostile email thread. Be explicit in both directions.
In scope should name real assets: *.example.com, your published npm and PyPI packages, your container images, your CLI binaries. If you ship open source libraries, say clearly that vulnerabilities in your dependencies are still reportable to you — you are the one shipping them to users, and pretending upstream bugs are not your problem is how supply chain incidents fester. We cover that dynamic in more depth in our post on dependency compromise timeline reconstruction.
Out of scope should be equally concrete: volumetric denial of service, social engineering of employees, physical attacks on offices, and findings from automated scanners with no proof of exploitability. That last one matters. Half the reports you will receive are Burp or Nuclei output pasted verbatim. A sentence like "reports must demonstrate a plausible security impact, not just a missing header" filters them politely.
Safe harbor that actually protects someone
This is where most policies collapse into weasel words. "We will not pursue legal action against researchers acting in good faith" is not safe harbor if the same page reserves the right to decide what good faith means after the fact.
The language researchers look for authorizes their activity under the specific statutes that scare them. In the US that means the Computer Fraud and Abuse Act and DMCA section 1201. Borrow from the disclose.io core terms, which exist precisely so nobody has to draft this from scratch:
- Research conducted under the policy is "authorized" for CFAA purposes.
- You waive DMCA 1201 claims for security research on your own products.
- You will not refer good-faith researchers to law enforcement.
- If a third party threatens legal action, you will state publicly that the research was authorized.
The US Department of Justice updated its CFAA charging policy in May 2022 to deprioritize good-faith security research, but a DoJ memo is not a defense a researcher can rely on. Your written authorization is. If your policy lacks this section, experienced researchers will assume the worst, and the people who ignore the risk anyway are not the reporters you want.
Publish SLAs and a disclosure clock you can survive
A policy without timelines is a suggestion box. Commit to numbers you can hit on a bad week:
| Stage | Commitment |
|---|---|
| Acknowledge receipt | 3 business days |
| Triage decision (valid/invalid/duplicate) | 10 business days |
| Fix or mitigation for critical severity | 30 days |
| Fix for high severity | 60 days |
| Coordinated public disclosure | 90 days from report |
The 90-day figure is not arbitrary — Google Project Zero normalized it a decade ago and CERT/CC uses 45 days. Researchers will hold you to whichever clock you publish, so publish one you believe. For your own sanity, state that the clock is a default, extendable by mutual agreement when a fix requires coordinated downstream updates. If the bug is in a library you publish, your fix window has to include time for your users to upgrade, which is where having an accurate inventory of what you actually ship — the kind of thing an SBOM pipeline gives you — turns a panicked weekend into a checklist.
Make intake developer-grade
Small things that decide whether a tired researcher finishes the report:
- Accept plain email. Mandatory web forms behind a login lose reports.
- Make PGP optional. Offer a key for those who want it, require it for no one.
- No mandatory NDA before you will read the report. This is a red flag researchers screenshot and post.
- Reply from a human, with a name, not a ticket auto-responder followed by silence.
- Say up front whether you pay bounties. "We do not offer monetary rewards but we credit researchers in our advisories" is respectable. Ambiguity is not.
Internally, wire the mailbox to your incident tooling on day one. A report that sits unread for two weeks because security@ forwarded to someone who left the company is a self-inflicted breach disclosure. When a valid report lands, the same muscle you use for incident response applies — assign an owner, reconstruct impact, and if the flaw shipped in released artifacts, treat version identification as the first work item.
Handling the report: credit, CVEs, and advisories
Close the loop publicly. Request a CVE (GitHub can assign them if you enable repository advisories and GitHub acts as your CNA), publish a GHSA or an advisory page, and credit the reporter by the name they choose. Teams running Safeguard typically attach the advisory to the affected package versions in their inventory so downstream consumers get flagged automatically, but whatever tooling you use, the principle holds: an advisory nobody can machine-read might as well be a tweet.
Frequently asked questions
What is the difference between a VDP and a bug bounty?
A VDP is the legal and procedural front door: scope, safe harbor, and reporting channel, with no payment implied. A bug bounty adds monetary rewards and usually a platform (HackerOne, Bugcrowd, Intigriti) to manage triage. Every organization shipping software should have a VDP; a bounty is optional and comes later.
Do I need security.txt if I already have a SECURITY.md?
Yes, they serve different audiences. SECURITY.md covers people who found you through a repository; /.well-known/security.txt covers people who found a bug in your running services and are probing your domain for a contact. RFC 9116 scanners and many researchers check the well-known path first.
How long should the disclosure deadline be?
90 days from report is the widely accepted default, with 30-day extensions negotiable for fixes that require coordinated downstream releases. Shorter than 45 days signals you have not thought about complex fixes; longer than 120 signals you plan to sit on reports.
Should safe harbor cover testing on production systems?
It should cover good-faith research against the assets you list in scope, which for most SaaS companies is production, because that is what researchers can reach. Pair it with explicit rules: no data exfiltration beyond proof of concept, no persistence, stop and report on first access to another user's data.