Skip to main content

security-advisory

Take a privately reported vulnerability through this repo's security advisory flow — board it, verify who owns the code path, accept or reject, fix it in the private fork, ship to every affected release line, publish, then turn the card into public tracking. Use when a vulnerability is reported privately; when deciding whether an advisory is ours to fix; when looking up or creating its private fork; when answering a reporter; or when a GHSA-titled board card needs handling.

Informations de source

Dépôt
modelcontextprotocol/inspector
Dernière activité de la source
5 novembre 2026 à 05:29
Langue détectée de SKILL.md
anglais
Étoiles
11 278
Forks
1 566

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Explorateur de fichiers
2 fichiers

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
security-advisory
description
Take a privately reported vulnerability through this repo's security advisory flow — board it, verify who owns the code path, accept or reject, fix it in the private fork, ship to every affected release line, publish, then turn the card into public tracking. Use when a vulnerability is reported privately; when deciding whether an advisory is ours to fix; when looking up or creating its private fork; when answering a reporter; or when a GHSA-titled board card needs handling.
disable-model-invocation
false
# Handling a security advisory Private vulnerability reporting is enabled on this repo and [`SECURITY.md`](../../../SECURITY.md) routes every report to it — the issue chooser deliberately has no security template, because a vulnerability report must not open a public issue. So an advisory never arrives as an issue, and until its fix is ready to land it must **not** become one. Two steps in this flow are **outward-facing, and both stay human-gated**: **accepting** an advisory (the reporter sees it) and **publishing** it (it becomes public, and there is no unpublish). Never automate either, never bulk-apply them, and never take either step because a checklist said to. Everything else here is mechanics. ⚠️ **A CVE and the credits are *choices made at publish time*, not effects of publishing.** Requesting a CVE is an optional action on the advisory, and a credit appears only when someone is explicitly added **and accepts** it. They are named here because they are the parts a maintainer must not forget — the reporter's credit especially, since nothing prompts for it — not because publishing performs them. Related: `/board-ops` (the card IDs and recipes) and `/issue-create`, for the labels, milestone and board that public tracking takes — **when the reviewed fix is ready to land** (step 5), since the fix lands through an ordinary public PR and that PR needs an issue to close. ⚠️ **How that tracking is created depends on the affected lines**, and only the v2 path is a conversion: a v2 issue is **converted** from the draft (filing one separately would duplicate both the issue and the card), while a v1 issue is **filed** on #11, because the draft is on #28 and cannot move there. Step 5 has the per-line sequence. ⚠️ **`/pr-flow` does not apply while the fix is in the private fork.** It requires a public issue and a public PR against the release branch — the disclosure this flow delays until the fix is ready. The fix is developed and reviewed inside the private fork (step 4); once it is ready, it lands through `/pr-flow` like any other change (step 5). ## The flow | # | Step | Gate | | --- | --- | --- | | 1 | Advisory lands in state `triage` → **draft card** on board #28 | Mechanical | | 2 | **Verify the claim** — who owns the code path, and **which release lines are affected** | Judgment | | 3 | Valid → **accept** (`triage` → `draft`); invalid → close with a reason | **Human only** | | 4 | Create the **private fork**, fix and review there | Mechanical | | 5 | Turn the card into **public tracking** (convert for v2; file on #11 for v1), land the fix through an **ordinary public PR** on every affected line, release each, then **publish** the advisory | Mechanical, except **publishing: human only** | | 6 | **After publication**, bring the public issue(s) up to date with what the advisory disclosed | Mechanical | ### 1. Board it as a draft card An advisory is private, so a public issue tracking it would disclose it before a fix exists. It therefore gets a **draft card** — the one documented exception to [`AGENTS.md`](../../../AGENTS.md#issue-driven-work-style)'s "every board item is a real GitHub issue". - **Title:** `[GHSA-xxxx-yyyy-zzzz] - <advisory summary>`. That `[GHSA-` prefix is not cosmetic: the board audit in `/issue-triage` keys its draft carve-out on it, so a card titled any other way is reported as a stray draft. - **Body:** `**Advisory:** <html_url>` on the first line, then severity and reported date. The link first, because a maintainer reading the card has no other route back to the private advisory. **The card may carry the full analysis**: the vulnerability description, the reproduction, the ownership verdict (step 2), the affected lines and the fix plan. It is often the only place that analysis can live. An advisory has no comment API, REST or GraphQL, so research cannot be scripted into its thread, and the card is the one place the rest of the team can see it. ⚠️ **That rests on one premise: access to board #28 is managed**, so the people who can read a card are the people who should be handling the advisory. Project access and advisory access are still separate permission sets, and the boards are private ([`/issue-triage`](../issue-triage/SKILL.md)). They are not public. **If board access is ever widened** beyond the people handling advisories, the premise fails. Revisit this rule then, and go back to keeping only the link and the triage metadata on the card. ⚠️ **None of this touches disclosure.** A card is private and a converted issue is not. The public issue still waits until the fix is ready to land (step 5), and an upstream finding still goes through the upstream's **private** channel (step 2). A detailed card is a reason to review its body before converting, not a reason to convert sooner. - **Status `Incoming`**, plus a **provisional** Priority scored with the `/issue-triage` rubric. `Incoming` is correct even though somebody clearly triaged it to make the card: nobody has approved shipping a fix yet, and a draft card has no milestone to carry the approval. ⚠️ **Provisional is not a hedge — it is the only honest score at this point.** The rubric's first axis is *severity*, and step 2 says ownership is established **before** severity, precisely because #2409 looked severe right up until it turned out not to be ours. At step 1 you have a report and nothing verified, so score what the report claims, mark it provisional in the body, and **re-score it at the end of step 2**, when you know whether the code is ours and which lines it reaches. An advisory that turns out to be upstream has its card deleted rather than re-scored (step 3). ⚠️ **Put the score's arithmetic in the draft body**, marked provisional and dated. `/issue-triage` says to record it as an issue comment, and a draft card has no comments — so without this the Priority is a bare word with nothing behind it, and the step-2 re-score cannot tell what it is revising. Use the same form as an ordinary `/issue-triage` score comment: each axis followed by its free-text justification ("Severity 3 — a real feature is broken…"), the bonuses you claimed, and the total. For an advisory the justification is the impact and the affected surface. That is exactly what makes the score arguable, so it belongs on the card with the number. Leave the provisional score in place when you re-score and add the new one under it, so the change of view is legible. ⚠️ **Set both fields.** The board audit's non-Issue check now exempts `[GHSA-` drafts, so a half-made card no longer trips it; the audit carries a narrow replacement check (see `/issue-triage`) and it is the only thing looking. The card is made **by hand**. There is no `PROJECT_TOKEN` in this org and `organization projects: write` is a permission `GITHUB_TOKEN` structurally cannot hold, so a board write is unreachable from Actions — the same constraint `AGENTS.md` records for the dependency sweeps. **Do not propose a nightly workflow for this;** that approach was tried and abandoned for exactly this reason. ```sh # --paginate: this endpoint returns 30 per page, and an inventory that silently # stops at the first page is worse than none — it reads as "nothing pending". gh api --paginate repos/modelcontextprotocol/inspector/security-advisories \ --jq '.[] | select(.state=="triage") | "\(.ghsa_id)\t\(.severity)\t\(.summary)"' ``` ### 2. Verify the claim — who owns the code path, and which lines it affects Before assessing severity, establish that the vulnerable code is **ours**. A report can be entirely accurate about behavior the Inspector merely exhibits because an SDK does it. ⚠️ **This is not hypothetical.** #2409 — a loopback/HTTPS-exemption finding — read as an Inspector defect and turned out to live in `@modelcontextprotocol/client` (`typescript-sdk#2591`). The reporter withdrew it. Had ownership been checked after the severity assessment rather than before, the fix would have been written against the wrong repo. So: reproduce it, find the code, and check whether that code is first-party or reached through a dependency. An advisory against upstream code is not ours to accept or publish. ⚠️ **"Upstream's problem" is not a reason to say it in public.** A genuine unfixed vulnerability handed to a public upstream issue is disclosed — by us, on someone else's behalf, before they have a fix. Route it through **that project's own private reporting channel** (its `SECURITY.md`, or its advisory form), and only reference a public upstream issue once the upstream has published. Where the reporter would rather carry it over themselves, say so and let them. #2409 took the benign version of this path: the reporter withdrew the report here and raised it upstream. #### Which release lines are affected — ask it here, not at merge time ⚠️ **An advisory is very nearly the only work the v1 line ever receives**, so this is exactly where assuming v2 does the most damage. `SECURITY.md` supports v1 for **security fixes only**, published under the `v1-latest` dist-tag, and its "What to Include" asks the reporter to state "whether it affects v2, v1, or both". Read what they said and then check it yourself — it is a request, not a required form field, so it is often absent and it is never authoritative when present. A v1-only advisory assumed to be v2 gets merged to a branch where the bug does not exist, and one affecting both lines leaves v1 **unpatched** while the advisory is published, which is the worst outcome this whole flow can produce. So the outcome of step 2 is a **set** of affected lines, and each one is shipped on its own terms: | Line | Branch | Flow | Publishes to | | --- | --- | --- | --- | | v2 | `v2/main` | `fix branch → v2/main → (milestone) main` | `latest` | | v1 | `v1/main` | `fix branch → v1/main`, flat — **no merge into `main`** | `v1-latest` | **The two lines publish independently under separate dist-tags, so a v1 fix is not forward-ported** — if v2 is affected too, that is a second fix on `v2/main`, not a merge. Branch names carry the version segment either way (`v1/fix/…`, `v2/fix/…`). **Now re-score the card's Priority**, replacing the provisional one from step 1. This is the first point at which the rubric's severity axis has anything solid under it: you know the code is ours, you have reproduced it, and you know how many lines it reaches — and "affects both lines" is itself a severity input the provisional score could not have had. ### 3. Accept, or close **Valid and ours → accept.** In the UI this is "Accept and open as draft"; it moves the advisory `triage` → `draft`. The state is readable as `state` and `submission.accepted` on the API object. **Then move the card `Incoming` → `Todo`.** On #28 the approval act is normally assigning a milestone, and a draft card cannot carry one — so for an advisory draft, **accepting the advisory is the approval**, and it is what licenses the card to leave `Incoming`. `AGENTS.md` records this as the advisory exemption to its `Incoming` ⇔ milestone invariant. The milestone arrives with the public issue in step 5, where the ordinary rule resumes. **Invalid, out of scope, or upstream → close** with a comment saying which, and why. A reporter who is told nothing reasonably assumes they were ignored. ⚠️ **Closing an advisory leaves its draft card behind — delete it.** Nothing shipped, so `Done` would be a false record and `Incoming` would claim work is still queued; `AGENTS.md` deletes a card in exactly this situation, and a rejected advisory's card is now invisible to the audit's non-Issue check by construction. The delete recipe is in `/board-ops`. ⚠️ **Accepting is a human act, always.** It is visible to the reporter and it commits this project to treating the report as a real vulnerability. Nothing in this skill authorizes taking it — surface the recommendation and let a maintainer click. ⚠️ **There is no comment API for security advisories.** Not in REST (the advisory object exposes no comments endpoint) and not in GraphQL (`RepositoryAdvisory` is not commentable, and no advisory-comment mutation exists). Comments are **UI-only**, so every exchange with a reporter is manual — you cannot script the reply, and you cannot read the thread back with `gh`. ### 4. The private fork Accepted advisories are fixed in a **private fork** GitHub creates for the advisory: a private repo named `<repo>-<ghsa-id>` in the org. ⚠️ **Read `private_fork` FIRST. The POST is not a probe — it CREATES one.** Calling it to "check whether a fork exists" makes one, in the org, which then needs cleaning up. This was learned the hard way. The script (`scripts/advisory-fork.mjs`, #2558) encodes that ordering: a bare run only reads, and it creates a fork only with the explicit `--create` flag and only when none exists: ```sh npm run advisory:fork -- --ghsa <GHSA_ID> # read-only probe npm run advisory:fork -- --ghsa <GHSA_ID> --create # create only if absent ``` ⚠️ **Deleting a private fork needs the `delete_repo` OAuth scope, which a default `gh` token does not carry.** So a fork created by mistake is not something you can quietly undo — it takes a re-scoped token or an admin in the UI. That asymmetry is the whole reason for the read-first rule above. Fix and review inside the fork. Its PRs and commits are private, so none of the normal public review flow applies there. Cut each fix branch from the base of the line it targets (`v2/main` for v2, `v1/main` for v1), **one per line step 2 found affected** — in step 5 that branch is pushed to the public repo unchanged and lands through an ordinary PR, so the fork's PRs are where the fix is _reviewed_, never where it is _merged_. ⚠️ **Move the card as the work moves.** `AGENTS.md`'s lifecycle applies to this card like any other: **`In Progress`** when the fix is started, **`In Review`** when the fork's PR is open. The card being private is not a reason to skip it — it is the reason to do it, since the fork is invisible to everyone who is not on the advisory, and this card is the only place the rest of the team can see the work exists at all. A card that sits in `Incoming` until it jumps to `Done` reports "unreviewed, nobody committed to it" for the entire time somebody is actively fixing it. ### 5. Land the fix on every affected line, release, publish Publish **after** the fix has shipped in a release, never before — publishing discloses the vulnerability, so doing it while users have no upgrade available hands out a working exploit. ⚠️ **"Shipped" means shipped on *every* affected line.** The two lines release independently under separate dist-tags, so v2 reaching `latest` says nothing about `v1-latest`. Publishing with one line still unpatched discloses a live vulnerability to the users who have no fix — and they are the users least able to move, since v1 is the deprecated line they are on because upgrading is hard. ⚠️ **The patch stops being secret when it reaches the public repo, not at publish — and no path changes that.** Merging from the private fork would put the same commits on `v2/main` or `v1/main` at merge time, so going through the fork buys no secrecy. The public flow below exposes the fix a little earlier, when its branch is pushed and its PR opened, and the converted issue exposes the card's title and body then too. So the window between landing and publish is not a period of secrecy to protect; it is a period of **exposure to anyone reading commits**, which is why it should be short. Land the fix close to the release rather than early, and publish as soon as every release is out. #### Landing through an ordinary public PR The fix lands the way any other change does: a public issue, a public branch, a `/pr-flow` PR with normal CI, the required `DCO` / `build` checks, the Copilot loop and normal merge rules. **No ruleset is touched.** This replaced merging from the private fork (1.0.3, #2753), which needed the `required_status_checks` rule removed from the line's ruleset for the merge — private forks run no Actions, so `v2/main`'s required `DCO` and `v1/main`'s required `build` stay "Expected" forever there — left the commits unchecked by CI, and merged all of an advisory's fork PRs (v2 and v1) at once. v2.14.0 and 1.0.4 shipped this way (#2872 / PR #2875, #2873 / PR #2876, #2874 / PR #2877 with its v1 half #2878 / PR #2879). When the fork's review is done, per affected line: 1. **Turn the card into public tracking.** Review the card body and **trim it before converting** — it becomes the body of an issue on a public repo, along with any analysis step 1 allowed onto it. The advisory is still private, so keep the advisory link and the fix, and take out anything that would hand a reader a working exploit (a PoC, reproduction steps, a reporter's details). Then: | Affected | Public issue | Labels | Milestone | Board | | --- | --- | --- | --- | --- | | v2 | **Convert** the draft card on #28 | `v2` + a type label | the release that will ship the fix | #28 — the converted card is already there | | v1 | **File** through `/issue-create` | `v1` + a type label | **none** — every milestone is a v2 release bucket | **#11**, Status only (no Priority field) | | both | **Convert** for v2 _and_ **file** for v1, as above | | | | ⚠️ **Convert the v2 issue — never file it.** GitHub's "Convert to issue" creates a **new** issue from the draft; there is no way to point an existing card at an issue you filed separately. Filing the v2 issue by hand and then converting produces two issues and two cards. Do **not** run `/issue-create`'s add-card step for the converted card: it already exists. **For an advisory affecting both lines**, v2 takes the conversion because the draft is already on #28 and v2 is the line with a milestone to record. The v1 issue is filed, which is not a contradiction: there is no second draft to convert. **Cross-link the two** so neither reads as the whole story. **For a v1-only advisory** the draft is on the wrong board and cannot be moved there by converting: file the `v1` issue on #11, then **delete** the #28 draft rather than converting it — a converted card would put a `v1` issue on #28, which the board audit reports as a wrong-board card. 2. **Push the fork branch to the public repo** under the standard name, `v2/fix/<N>-<slug>` or `v1/fix/<N>-<slug>`, where `<N>` is that line's issue from (1). Push the fork's commits unchanged; they are already reviewed and must already carry their DCO signoff. 3. **Open an ordinary PR** against `v2/main` / `v1/main` with `Closes #<N>` as the body's first line, and follow `/pr-flow` from its step 6 on: link the issue, move the card to In Review, run the Copilot loop. Say in the body that the fix was developed and reviewed in the advisory's private fork, and keep the description to the fix — what the advisory will disclose is the advisory's to say. When several fix branches were chained on top of each other in the fork, open them as **stacked** PRs (`/pr-flow`, step 2) rather than one combined PR. 4. **Merge, close the issue and move its card to `Done`** (`/pr-flow`, step 9), then **close the fork's PRs unmerged**. The fix is on the line through the public PR; merging the fork's copy would land it twice. **Then release on every affected line.** Do not hand this off to the release skill. It is `disable-model-invocation: true`, so a pointer to it from here is a dead end for the model anyway — a maintainer invokes `/release` themselves. Say which lines need a release and stop there. A v1 fix takes no merge into `main` at all and publishes straight from `v1/main`. ⚠️ **Publishing is irreversible and human-gated.** It makes the advisory public, and there is no undo. Same rule as accepting: recommend, never perform. **Before publishing, do the two things publishing will not do for you:** request the **CVE** (optional, and the advisory is the only place to ask) and **add the reporter to the credits** — a credit is an explicit addition the person then has to accept, so an unadded reporter is simply never credited, and that is the failure nobody notices because nothing reports it. ### 6. After publication, bring the public issues up to date The public tracking already exists — it was created in step 5 and its issues closed when the PRs merged — so there is nothing to convert or file here. What publication changes is what may be said: the issue bodies were trimmed to what was safe while the advisory was private. **Edit them only where that left them misleading**, typically to point at the published advisory and add what it now discloses. The cards stay in `Done`. ## API facts worth not re-deriving All verified against the live API. | Thing | Fact | | --- | --- | | States | `triage` → `draft` (accepted) → `published`; or `closed` | | Accepted? | `submission.accepted` on the advisory object, alongside `state` | | Private fork | `POST …/security-advisories/{ghsa_id}/forks` → `202`, private repo `<repo>-<ghsa-id>` in the org |
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub