How-to › Release and secure packages

How to write a CVE response runbook for an SDK#

Take a private vulnerability report to a fixed release in every language you ship, then check the advisory against the OSV schema and your SDK list.

Audience
Library maintainer
Level
advanced
Topic
Secure the supply chain
Verified

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

ApproachWhen it fitsWhat it costs youWhen to pick something else
GitHub Security AdvisoriesThe SDK repositories live on GitHub and you want intake, a CVE id, a private fork, and Dependabot alerts in one placeYour disclosure process lives on one platform, and the private fork runs no status checksThe code is not on GitHub, or another CNA already covers your project
HackerOneA company SDK with enough reports to need triage help and a published disclosure policyReports live on another company’s platform, and CVE requests are an opt-in feature not in every editionA small open source SDK that receives a handful of reports a year
MITRE CVE request formA security mailing list you run yourself, and no CNA that covers the projectYou write the CVE record, the advisory, and every ecosystem’s range yourselfGitHub or another CNA can assign the id from the advisory you already wrote
OSV databaseYou want scanners in every ecosystem to match the advisory by package and versionIt aggregates other databases, so a record reaches it through GHSA, a language database, or converted CVE recordsNever 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.

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

  1. 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

  2. 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

  3. 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

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Listen to Fireside, the Voxgig podcast

Since 2018, Richard Rodger has talked with developer advocates, event organizers, and the people who build developer-facing companies about the specifics of the work.