Repository navigation
chore(security): add security policy, dependabot config, and config key guards #1415
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c9db1f0
9a7d665
89240c1
332fefe
c624e82
dbcb8ee
6df9fce
9d98af0
af52321
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| "@fission-ai/openspec": patch | ||
| --- | ||
|
|
||
| Reject config key paths that reach the prototype chain, and update the bundled `yaml` dependency. | ||
|
|
||
| `openspec config set --allow-unknown __proto__.polluted <value>` reported success and assigned onto `Object.prototype` for the rest of the process. `--allow-unknown` was meant to relax the known-key check only, but it skipped every key check, so `__proto__`, `constructor`, and `prototype` segments reached the nested-write helper. Those segments are now rejected in `config set` whether or not `--allow-unknown` is passed, and `setNestedValue` / `deleteNestedValue` refuse them regardless of caller. Ordinary keys such as `featureFlags.myFlag` behave exactly as before. | ||
|
|
||
| The `yaml` runtime dependency moves from 2.8.2 to 2.9.0, picking up the fix for a stack overflow on deeply nested input (GHSA / advisory patched in 2.8.3). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "@fission-ai/openspec": patch | ||
| --- | ||
|
|
||
| Parse spec headings in linear time when the title is padded with whitespace. | ||
|
|
||
| Building the reference index read the first Purpose line with a regex that backtracked quadratically on a heading full of spaces: 10,000 characters of padding took 60ms, and 100,000 would have taken roughly six seconds. The heading scan is now hand-rolled and linear. Behavior is unchanged — the replacement was checked against the old implementation across 303,000 generated inputs, including CommonMark closing sequences (`## Purpose ##`), seven-hash lines, and headings with no space after the hashes. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| version: 2 | ||
|
|
||
| updates: | ||
| # Published CLI package | ||
| - package-ecosystem: npm | ||
| directory: / | ||
| schedule: | ||
| interval: weekly | ||
| day: monday | ||
| # Let a freshly published version sit before adopting it. Security updates | ||
| # ignore the cooldown, so this only delays routine bumps — long enough for a | ||
| # compromised release to be yanked before it reaches this repo. | ||
| cooldown: | ||
| default-days: 7 | ||
| semver-major-days: 30 | ||
| semver-minor-days: 7 | ||
| semver-patch-days: 3 | ||
| open-pull-requests-limit: 5 | ||
| commit-message: | ||
| prefix: chore | ||
| include: scope | ||
| groups: | ||
| production-dependencies: | ||
| dependency-type: production | ||
| update-types: | ||
| - minor | ||
| - patch | ||
| development-dependencies: | ||
| dependency-type: development | ||
| update-types: | ||
| - minor | ||
| - patch | ||
|
|
||
| # Documentation site (not published to npm) | ||
| - package-ecosystem: npm | ||
| directory: /website | ||
| schedule: | ||
| interval: weekly | ||
| day: monday | ||
| # Let a freshly published version sit before adopting it. Security updates | ||
| # ignore the cooldown, so this only delays routine bumps — long enough for a | ||
| # compromised release to be yanked before it reaches this repo. | ||
| cooldown: | ||
| default-days: 7 | ||
| semver-major-days: 30 | ||
| semver-minor-days: 7 | ||
| semver-patch-days: 3 | ||
| open-pull-requests-limit: 3 | ||
| commit-message: | ||
| prefix: chore | ||
| include: scope | ||
| groups: | ||
| website-dependencies: | ||
| patterns: | ||
| - "*" | ||
| update-types: | ||
| - minor | ||
| - patch | ||
|
|
||
| # CI workflow actions | ||
| - package-ecosystem: github-actions | ||
| directory: / | ||
| schedule: | ||
| interval: weekly | ||
| day: monday | ||
| # Actions are not semver-versioned the way packages are, so this ecosystem | ||
| # accepts default-days only. | ||
| cooldown: | ||
| default-days: 7 | ||
| commit-message: | ||
| prefix: ci | ||
| groups: | ||
| github-actions: | ||
| patterns: | ||
| - "*" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,19 +24,19 @@ jobs: | |
| # (GITHUB_TOKEN cannot trigger workflows by design) | ||
| - name: Generate GitHub App Token | ||
| id: app-token | ||
| uses: actions/create-github-app-token@v3 | ||
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 | ||
| with: | ||
| app-id: ${{ vars.APP_ID }} | ||
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | ||
|
|
||
| - uses: actions/checkout@v5 | ||
| - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 | ||
| with: | ||
| fetch-depth: 0 | ||
| token: ${{ steps.app-token.outputs.token }} | ||
|
Comment on lines
+32
to
35
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/workflows/release-prepare.yml"
echo "== outline =="
ast-grep outline "$FILE" --view expanded || true
echo
echo "== relevant lines =="
nl -ba "$FILE" | sed -n '1,220p'
echo
echo "== checkout and git push references =="
rg -n "actions/checkout|git push|persist-credentials|token:|github.token|app-token|changeset|release" "$FILE"Repository: Fission-AI/OpenSpec Length of output: 286 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/workflows/release-prepare.yml"
echo "== file metadata =="
wc -l "$FILE"
echo
echo "== lines 1-140 =="
sed -n '1,140p' "$FILE" | nl -baRepository: Fission-AI/OpenSpec Length of output: 279 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/workflows/release-prepare.yml"
wc -l "$FILE"
sed -n '1,180p' "$FILE" | nl -ba
rg -n "actions/checkout|git push|persist-credentials|token:|github.token|app-token|changeset|release" "$FILE"Repository: Fission-AI/OpenSpec Length of output: 239 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/workflows/release-prepare.yml"
echo "== top of file (1-120) =="
sed -n '1,120p' "$FILE" | awk '{printf "%4d %s\n", NR, $0}'
echo
echo "== bottom of file (121-220) =="
sed -n '121,220p' "$FILE" | awk '{printf "%4d %s\n", NR+120, $0}'
echo
echo "== credential and push references =="
rg -n "actions/checkout|persist-credentials|git push|git config|token:|github.token|app-token|changeset|release" "$FILE"Repository: Fission-AI/OpenSpec Length of output: 10396 Disable credential persistence on the prepare checkout ( 🧰 Tools🪛 zizmor (1.26.1)[warning] 32-35: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false (artipacked) 📍 Affects 1 file
🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| - uses: pnpm/action-setup@v6 | ||
| - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 | ||
|
|
||
| - uses: actions/setup-node@v6 | ||
| - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 | ||
| with: | ||
| node-version: '24' # Node 24 includes npm 11.5.1+ required for OIDC | ||
| cache: 'pnpm' | ||
|
|
@@ -47,7 +47,7 @@ jobs: | |
| # Opens/updates the Version Packages PR; publishes when the Version PR merges | ||
| - name: Create/Update Version PR | ||
| id: changesets | ||
| uses: changesets/action@v1 | ||
| uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1 | ||
| with: | ||
| title: 'chore(release): version packages' | ||
| createGithubReleases: true | ||
|
|
@@ -70,13 +70,13 @@ jobs: | |
| if: github.repository == 'Fission-AI/OpenSpec' && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
| - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - uses: pnpm/action-setup@v6 | ||
| - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 | ||
|
|
||
| - uses: actions/setup-node@v6 | ||
| - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 | ||
| with: | ||
| node-version: '24' # Node 24 includes npm 11.5.1+ required for OIDC | ||
| cache: 'pnpm' | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| name: Security | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - '**/package.json' | ||
| - '**/pnpm-lock.yaml' | ||
| - '.github/workflows/security.yml' | ||
| pull_request: | ||
| branches: [main] | ||
| schedule: | ||
| # Weekly, so a newly published advisory surfaces even with no commits. | ||
| - cron: '17 6 * * 1' | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
Comment on lines
+17
to
+18
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== workflow file =="
git ls-files .github/workflows/security.yml
echo
cat -n .github/workflows/security.yml | sed -n '1,220p'
echo
echo "== search for dependency-review-action usage =="
rg -n "dependency-review-action|comment-summary-in-pr|pull-requests: write|permissions:" .github/workflows -S
echo
echo "== candidate docs references in repo =="
rg -n "dependency-review-action|comment-summary-in-pr" -S .Repository: Fission-AI/OpenSpec Length of output: 3310 🌐 Web query:
💡 Result: To use the comment-summary-in-pr feature in the actions/dependency-review-action, you must explicitly grant the pull-requests: write permission to your GitHub Actions workflow or job [1][2]. By default, the action does not post comments, and the comment-summary-in-pr setting is disabled (set to never) [1][3]. When enabled (by setting it to always or on-failure), the action requires write access to pull requests to create or update the summary comment [1][2]. With each execution, the action will overwrite the existing comment in the pull request [1][4]. You can configure these permissions in your workflow YAML file as follows [2][5]: permissions: contents: read pull-requests: write jobs: dependency-review: runs-on: ubuntu-latest steps: - name: Dependency Review uses: actions/dependency-review-action@v4 with: comment-summary-in-pr: always Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
cat -n .github/workflows/security.yml | sed -n '1,220p'
rg -n "dependency-review-action|comment-summary-in-pr|pull-requests: write|permissions:" .github/workflows -SRepository: Fission-AI/OpenSpec Length of output: 3168 Grant
🤖 Prompt for AI Agents |
||
|
|
||
| concurrency: | ||
| group: security-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| # Blocks a pull request that introduces a vulnerable or badly licensed dependency. | ||
| dependency-review: | ||
| name: Dependency Review | ||
| if: github.event_name == 'pull_request' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| # No PR comment: that needs `pull-requests: write`, which a fork's token | ||
| # never gets. The failed check plus its log is the signal. | ||
| - name: Review dependency changes | ||
| uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4 | ||
| with: | ||
| fail-on-severity: high | ||
|
|
||
| audit: | ||
| name: Audit | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Setup pnpm | ||
| uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 | ||
|
|
||
| # No dependency cache: `pnpm audit` reads the lockfile, nothing is installed, | ||
| # so a cache-save step would fail on the missing store path. | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 | ||
| with: | ||
| node-version: '20.19.0' | ||
|
|
||
| # Advisory on pull requests: a newly published advisory should not stop an | ||
| # unrelated change, and the step depends on registry availability. | ||
| # Blocking everywhere else — on the weekly schedule and on pushes to main | ||
| # — so a high-severity advisory in a shipped dependency still fails a run | ||
| # even when no dependency changed. | ||
| - name: Audit published dependencies | ||
| continue-on-error: ${{ github.event_name == 'pull_request' }} | ||
| run: pnpm audit --prod --audit-level high | ||
|
|
||
| # Build and test tooling never reaches an installed copy of OpenSpec, so an | ||
| # advisory here is a scheduled-update item. | ||
| - name: Audit build and test tooling | ||
| continue-on-error: true | ||
| run: pnpm audit --audit-level high | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # Security Policy | ||
|
|
||
| ## Reporting a vulnerability | ||
|
|
||
| Report privately through [GitHub Security Advisories](https://github.com/Fission-AI/OpenSpec/security/advisories/new). Please don't open a public issue for a suspected vulnerability. | ||
|
|
||
| Include what you can: affected version, reproduction steps, and the impact you believe it has. We aim to acknowledge within 3 business days and to ship a fix or a decision within 30 days. Valid reports are credited in the advisory unless you'd rather stay anonymous. | ||
|
|
||
| ## Supported versions | ||
|
|
||
| Fixes ship in the latest published version on npm. Older versions are not patched — upgrade to pick up a fix. | ||
|
|
||
| ## Threat model | ||
|
|
||
| OpenSpec is a local command-line tool. It has no server, no network listener, and no privileged daemon. It reads and writes markdown under the directory you run it in, using paths you supply, with your own user permissions. It sends anonymous usage telemetry, which you can disable with `OPENSPEC_TELEMETRY=0`. | ||
|
|
||
| That shapes what is and isn't a vulnerability here: | ||
|
|
||
| | In scope | Out of scope | | ||
| | --- | --- | | ||
| | Code execution triggered by parsing a spec, config, or template file | Reading or writing a file path you passed to the CLI yourself | | ||
| | Escaping the directory OpenSpec was pointed at, via untrusted input | Static-analysis findings on file-path joins with no untrusted input | | ||
| | Leaking credentials or file contents through telemetry or logs | Vulnerabilities in devDependencies that don't ship in the published package | | ||
| | Prototype pollution or injection reachable from a config or spec file | Denial of service against your own machine using your own input | | ||
|
|
||
| If you think something sits on the boundary, report it and we'll work it out together. | ||
|
|
||
| ## Published package contents | ||
|
|
||
| The `openspec` npm package publishes `dist/`, `bin/`, `schemas/`, and `scripts/postinstall.js`. Build and test tooling (vite, rollup, vitest, eslint, and their transitive dependencies) is not published. Scanners that read `pnpm-lock.yaml` without separating dependency scope will report advisories for packages that never reach an installed copy of OpenSpec. | ||
|
|
||
| You do not have to take that on trust — install the package and look: | ||
|
|
||
| ```sh | ||
| npm install @fission-ai/openspec | ||
| ls node_modules | grep -E '^(vite|rollup|vitest|eslint|js-yaml|minimatch)$' # no matches | ||
| ``` | ||
|
|
||
| `pnpm audit --prod` in this repository reports the same scope, and CI runs it on every pull request. | ||
|
|
||
| ## What the CLI does on your machine | ||
|
|
||
| | Surface | Behavior | | ||
| | --- | --- | | ||
| | Install script | `scripts/postinstall.js` prints one line suggesting shell completions. It makes no network request, writes no files, and runs no shell. Completions are opt-in via `openspec completion install`. | | ||
| | Running other programs | Every call that goes through a shell uses a fixed literal (`which gh`, `gh auth status`). Anything carrying your input — issue text, editor paths, workset commands — uses an argument array with `shell: false`. | | ||
| | Telemetry | Command name, OpenSpec version, and a locally generated random UUID. No file paths, no file contents, no environment, no hostname, and IP capture is explicitly disabled. Opt out with `OPENSPEC_TELEMETRY=0` or `DO_NOT_TRACK=1`; it is off in CI automatically. | | ||
| | Network | Only telemetry, and only when enabled. Reading, writing, and validating specs is entirely local. | | ||
|
|
||
| ## Automated checks | ||
|
|
||
| | Tool | Covers | | ||
| | --- | --- | | ||
| | [CodeQL](https://github.com/Fission-AI/OpenSpec/security/code-scanning) | Static analysis on every push and pull request to `main` | | ||
| | [Dependabot](https://github.com/Fission-AI/OpenSpec/security/dependabot) | Dependency advisories plus weekly update pull requests for the CLI, the docs site, and CI actions | | ||
| | Dependency review | Blocks a pull request that introduces a high-severity dependency | | ||
| | Secret scanning | Enabled on the repository, including push protection | | ||
| | `pnpm audit` | Published dependencies are audited on every pull request, on pushes to `main`, and weekly. Advisory on pull requests so an unrelated change is not blocked; failing elsewhere, so a new advisory surfaces even when no dependency changed. Build tooling is always advisory. | | ||
| | Pinned actions | Every GitHub Action runs from a commit SHA, so a moved tag cannot change what CI executes | | ||
|
|
||
| Alerts are triaged against the threat model above, so a finding in build-only tooling is fixed on the normal update cadence rather than treated as an incident. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: Fission-AI/OpenSpec
Length of output: 9997
🏁 Script executed:
Repository: Fission-AI/OpenSpec
Length of output: 31116
🌐 Web query:
actions/create-github-app-token permissions no permission-* inputs grants all available permissions💡 Result:
The actions/create-github-app-token GitHub Action does not have a single input that automatically grants all available permissions to the generated token [1][2]. Instead, it uses a granular system where you can specify permissions using individual inputs prefixed with permission- (e.g., permission-issues: write, permission-pull-requests: read) [1][3]. By default, if you do not provide any permission-* inputs, the generated token automatically inherits all permissions currently granted to the GitHub App installation [1][3][4]. This default behavior effectively grants all permissions the app possesses, which is why the action recommends explicitly listing only the permissions required for your specific use case to follow the principle of least privilege [1][4]. If you were to manually list every available permission to mimic granting all permissions, you would need to define each required permission as a separate input in your workflow YAML [1][3]. Please note that requesting a permission in your workflow that the GitHub App installation itself does not have will result in an error [1][4][5].
Citations:
Limit the GitHub App token to the permissions this release needs.
Without any
permission-*inputs,actions/create-github-app-tokeninherits every permission granted to the app installation. Narrow it to the small set this job actually uses, so checkout and Changesets don't get the full installation grant.🧰 Tools
🪛 zizmor (1.26.1)
[error] 27-27: dangerous use of GitHub App tokens (github-app): app token inherits blanket installation permissions
(github-app)
🤖 Prompt for AI Agents
Source: Linters/SAST tools