A researcher reports that your SDK forwards the caller’s API token when it follows a redirect to another host. You fix the TypeScript package, ship 3.2.1, and publish an advisory naming it. The same transport was generated into five other languages. Python, Go, Java, PHP, and Ruby users get no alert, and anyone pinned to 2.x gets an alert whose only fix is a major upgrade.
What you get
You will end up with a runbook that takes a private report to a fixed release in every language you ship, and a checker for the advisory. This is for you if you publish SDKs in several languages from one spec.
Short answer
Write the runbook as a checklist from private report to published advisory. Take reports through
SECURITY.md and private vulnerability reporting, and score them with a full CVSS vector. Ask the
CNA that covers you for a CVE id, and fix in a private fork with a backport to every supported major.
Before publishing, check the advisory against the OSV schema and against the list of every SDK
generated from the same spec.
You will need
A published SDK with a supported-versions policy, because the policy decides which majors get a
backport. Node 22 or later for the checker, with Ajv and ajv-formats from the sample’s lockfile, and
version 1.9.2 of the OSV schema, vendored beside it. The ids,
package names, and versions in the sample belong to a fictional Acme SDK.
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| GitHub Security Advisories | The SDK repositories live on GitHub and you want intake, a CVE id, a private fork, and Dependabot alerts in one place | Your disclosure process lives on one platform, and the private fork runs no status checks | The code is not on GitHub, or another CNA already covers your project |
| HackerOne | A company SDK with enough reports to need triage help and a published disclosure policy | Reports live on another company’s platform, and CVE requests are an opt-in feature not in every edition | A small open source SDK that receives a handful of reports a year |
| MITRE CVE request form | A security mailing list you run yourself, and no CNA that covers the project | You write the CVE record, the advisory, and every ecosystem’s range yourself | GitHub or another CNA can assign the id from the advisory you already wrote |
| OSV database | You want scanners in every ecosystem to match the advisory by package and version | It aggregates other databases, so a record reaches it through GHSA, a language database, or converted CVE records | Never instead of the others; it is where their advisories end up |
GitHub’s flow is the fast one. Private reporting, a CVE request usually reviewed within 72 hours, and Dependabot alerts arrive together. The price is that disclosure depends on one platform. A self-run process, a mailing list plus the MITRE form, keeps the process yours. It costs you the advisory format, the CVE record, and the OSV data, all written by hand. HackerOne sits between the two: triage help and a CNA, on another company’s platform.
Take the report privately
Publish where a report goes before one arrives. GitHub reads
a security policy
from SECURITY.md, and the sample’s version names the supported majors and says one report covers
all six languages. Turn on
private vulnerability reporting
in the repository that holds the spec or the generator, not in one language’s repository. The
report then lands where the fix for all six will be made.
Score it, then ask for an id
Record the CVSS vector before the conversation about severity starts, so the argument is about
metrics rather than adjectives. The
CVSS 3.1 specification requires all
eight base metrics in a vector string. The OSV schema’s pattern does not enforce that, so
CVSS:3.1/AV:N/AC:H is valid OSV but an unusable score, and the checker below counts the metrics
itself.
GitHub is a CNA, so a repository advisory can request a CVE id, and GitHub says it usually reviews the request within 72 hours. It cannot assign one if another CNA covers your project. With no CNA, a self-run process asks MITRE through the CVE Program’s request page.1 HackerOne is a CNA too, for programs on an edition that includes CVE requests.2
Fix every language in private
GitHub’s temporary private fork keeps the fix out of public view, and it changes the rules you are used to. Status checks do not run on its pull requests, branch protection is not enforced, and every open pull request merges at once. For an SDK family that means running each language’s test suite by hand and pasting the result into the advisory. Then make the backport. Every major in your supported-versions policy needs its own fixed release, or its users get told to jump a major to be safe.
Check the advisory covers every SDK
The advisory is the artifact scanners read, and it is where the forgotten language shows. The
checker validates against the OSV schema first, then walks sdks.json, the list of every package
generated from the spec, and asks each one for a range.
for (const major of majors) {
const lo = `${major}.0.0`
const hi = `${major + 1}.0.0`
const reachesMajor = (s) =>
s.fixed ? cmp(s.fixed, lo) > 0 : s.lastAffected ? cmp(s.lastAffected, lo) >= 0 : true
const hits = spans.filter((s) => cmp(s.introduced, hi) < 0 && reachesMajor(s))
for (const s of hits) {
if (s.lastAffected) problems.push(`${label}: ${major}.x ends at last_affected ${s.lastAffected}; give the fixed version`)
else if (!s.fixed) problems.push(`${label}: ${major}.x is affected from ${s.introduced} with no fixed version`)
else if (cmp(s.fixed, hi) >= 0) problems.push(`${label}: ${major}.x has no fix of its own; the first fixed version is ${s.fixed}`)
}
}
Each supported major is a window. A span that reaches into the window must close inside it, with a
fixed event, which the
OSV specification prefers over last_affected because it
names the version that contains the fix. Run it on the complete advisory and on the one from the
problem statement:
node check-advisory.mjs advisory.json advisory-npm-only.json
advisory.json: GHSA-xxxx-xxxx-xxxx, 7 affected packages
OSV schema 1.9.2: valid
ok TypeScript @acme/sdk fixed in 2.8.4, 3.2.1
ok Python acme-sdk fixed in 2.8.4, 3.2.1
ok Go github.com/acme/sdk-go/v2 fixed in 2.8.5
ok Go github.com/acme/sdk-go/v3 fixed in 3.2.1
ok Java com.acme:acme-sdk fixed in 2.8.4, 3.2.2
ok PHP acme/sdk fixed in 2.8.4, 3.2.1
ok Ruby acme-sdk fixed in 2.8.4, 3.2.1
advisory-npm-only.json: GHSA-xxxx-xxxx-xxxx, 1 affected packages
OSV schema 1.9.2: valid
ok TypeScript @acme/sdk fixed in 2.8.4, 3.2.1
FAIL Python (PyPI acme-sdk): no affected entry
FAIL Go (Go github.com/acme/sdk-go/v2): no affected entry
FAIL Go (Go github.com/acme/sdk-go/v3): no affected entry
FAIL Java (Maven com.acme:acme-sdk): no affected entry
FAIL PHP (Packagist acme/sdk): no affected entry
FAIL Ruby (RubyGems acme-sdk): no affected entry
Both files are valid OSV. The schema describes the shape of a record and has no way to know that six packages share one transport. Go appears twice because Go puts a major version suffix on the module path from v2 on, so each major is a separate package to a scanner. Java’s 3.x fix is 3.2.2 and the rest are 3.2.1; the languages release on their own numbers, and the advisory has to say so.
Turn the runbook into a checklist
Copy the checklist into the draft advisory as its first comment, and tick it as you go. A struck line with a reason is better than a deleted one, because the checklist is also the record of what was done.
## Fix (private)
- [ ] Fix made in the temporary private fork; status checks do not run there, so each SDK's
test suite was run by hand and the output pasted into the advisory
- [ ] Backport made to every supported major in sdks.json
- [ ] Fixed version number reserved for each package and each major
## Release (disclosure day)
- [ ] All fixed packages published, in every ecosystem, before the advisory goes public
- [ ] `node check-advisory.mjs advisory.json` passes: OSV schema valid, every package covered
The full file adds intake, triage, and a section for the week after, which checks that the record reached the GitHub Advisory Database and osv.dev for each package.
Check it worked
Run the tests. Fourteen cases cover the missing language, the missing backport, an entry with no
ranges, a range with no fix, and last_affected. Others cover a version the checker refuses to
guess at, a misspelled ecosystem, and the incomplete CVSS vector the schema lets through. The last
adds an entry that lists versions without ranges, beside a GIT range. The checker reads both
without crashing or printing a commit as a fixed version.
node --test
1..14
# tests 14
# suites 0
# pass 14
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 314.656703
Then run the checker on your real draft. The literal to look for is valid on the schema line
and no FAIL lines under it.
When it goes wrong
The fix shipped in 3.x only, so 2.x users are told to upgrade across a major in the middle of an incident:
node check-advisory.mjs advisory-no-backport.json
FAIL TypeScript (npm @acme/sdk): 2.x has no fix of its own; the first fixed version is 3.2.1
Dependabot alerts users with no version to move to. GitHub’s documentation says an advisory published without a fix version alerts users anyway, without offering a safe version. Publish the fixed packages first and the advisory second.
The checker refuses 2.8.4.post1. Python and Java version strings are not always three numbers,
and a guessed comparison produces a wrong range. Teach parseVersion your ecosystem’s scheme, or
release fixes with plain numbers.
When not to do this
Do not write a separate runbook per language. The vulnerability lives in the generator or the spec, so the response has to start there, and six runbooks produce six advisories with six different ranges.
Do not publish the advisory before every ecosystem has its fixed package. The advisory is a map to the vulnerable code for anyone reading it, and a language without a release is the one they will look at.
Do not run HackerOne for an SDK that gets three reports a year. A SECURITY.md, private
vulnerability reporting, and this checklist cover that volume, and the platform’s launch rules
assume a program with traffic.3
Do not treat the OSV schema check as the review. It confirms the record is well formed, which says nothing about whether the ranges are true.
Related how-tos
Last verified
Verified 2026-10-15 against Node 22.22.0, ajv 8.20.0, ajv-formats 3.0.1 and version 1.9.2 of
the OSV schema. Every output block is what the command preceding it printed. The advisories are
fixtures for a fictional SDK, checked as files rather than submitted to GitHub, MITRE, HackerOne,
or OSV.
Footnotes
-
The address most guides give for that form,
cveform.mitre.org, redirects to a page titled Contacting the CVE Program. The page calls itself an upgrade to the legacy form and says the two run in parallel for now, with the legacy one due to be retired in the coming months. ID requests are filed under MITRE CNA-LR, the CNA of Last Resort. The name describes the order in which a supplier is expected to try things. ↩︎ Back to text -
The CVE requests article says HackerOne assigns the id when the advisory is posted publicly, and publishes it to MITRE. An organization opts in by talking to its Customer Success Manager. The request for an identifier begins with a request for a conversation. ↩︎ Back to text
-
HackerOne’s program setup guide keeps a disclosure program in a controlled launch until it has received at least 10 reports and invited 100 hackers. Only then does the public launch button appear. A program for a quiet SDK can wait a long time for its tenth report. ↩︎ Back to text