Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,24 @@ jobs:
steps:
- name: Checkout Repo
uses: actions/checkout@v6

- uses: pnpm/action-setup@v6
name: Install pnpm
with:
run_install: false
# Supply-chain hardening — never cache the pnpm store; a poisoned
# cache entry would execute in this credential-bearing workflow.
cache: false

- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'pnpm'
# No `cache:`, and package-manager-cache disabled. release.yml
# publishes to npm (NPM_TOKEN + OIDC) and must not restore the
# GitHub Actions cache — a cache-poisoning / supply-chain vector.
# Enforced by .github/workflows/tests-supply-chain.yml.
package-manager-cache: false

# node-pty's install hook falls back to `node-gyp rebuild` when no
# linux-x64 prebuild matches. pnpm/action-setup v6 no longer ships
Expand All @@ -50,4 +57,4 @@ jobs:
commitMode: 'github-api'
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
80 changes: 80 additions & 0 deletions .github/workflows/tests-supply-chain.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Test supply chain security

# Supply-chain gate: asserts that release.yml (and this workflow) never
# restore the GitHub Actions cache. A workflow that both restores a cache
# and holds publish credentials is a cache-poisoning target — see
# https://adnanthekhan.com/2024/05/06/the-monsters-in-your-build-cache-github-actions-cache-poisoning/
#
# The check (scripts/lint-no-workflow-caching.mjs) requires caching to be
# disabled *explicitly*, not just left at its default:
# - `pnpm/action-setup` must set `cache: false`
# - `actions/setup-node` must set `package-manager-cache: false`
# - no `cache:` input and no `actions/cache` step anywhere
# See the "CI/CD Supply-Chain Hardening" section of SECURITY.md.
#
# Deliberately minimal:
# - GitHub-hosted runner (no Blacksmith transparent cache)
# - contents:read only
# - no secrets
# - no caching

on:
push:
branches:
- main
paths:
- '.github/workflows/release.yml'
- '.github/workflows/tests-supply-chain.yml'
- 'scripts/lint-no-workflow-caching.mjs'
- 'scripts/__tests__/lint-no-workflow-caching.test.mjs'
- 'scripts/__tests__/fixtures/lint-no-workflow-caching/**'
pull_request:
branches:
- '**'
paths:
- '.github/workflows/release.yml'
- '.github/workflows/tests-supply-chain.yml'
- 'scripts/lint-no-workflow-caching.mjs'
- 'scripts/__tests__/lint-no-workflow-caching.test.mjs'
- 'scripts/__tests__/fixtures/lint-no-workflow-caching/**'

permissions:
contents: read

jobs:
verify-no-caching-in-release-workflows:
name: Verify no caching in release workflows
runs-on: ubuntu-latest

steps:
- name: Checkout Repo
uses: actions/checkout@v6
Comment on lines +50 to +51
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Policy enforcement is mutable by the PR under test.

On pull_request, this workflow checks out PR code and then runs pnpm run lint:workflow-cache, so the same PR can weaken the lint script/tests and still pass while reintroducing cache usage. Move this gate to an immutable source (for example: base-branch pull_request_target policy execution, or a pinned reusable workflow/action that is not editable in the same PR).

Also applies to: 79-80

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/tests-supply-chain.yml around lines 50 - 51, The workflow
currently checks out PR code using the actions/checkout step (uses:
actions/checkout@v6) before executing the mutable gate pnpm run
lint:workflow-cache, which allows a PR to modify the policy; change the gate to
run from an immutable source instead—either run the policy check in a
pull_request_target trigger or invoke a pinned reusable workflow/action that
cannot be altered by the PR (instead of using the repo checkout + pnpm run
lint:workflow-cache in the PR context); update the steps referencing
actions/checkout@v6 and the pnpm run lint:workflow-cache invocation (also at the
repeated block around lines 79-80) to call the immutable policy executor so PR
changes cannot weaken the check.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid concern, but will be addressed in a follow up PR, by splitting this check out into a re-usable GitHub Action.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@auxesis, understood — splitting it into a reusable GitHub Action is a solid approach since the action can be pinned to a ref that the PR cannot modify. Would you like me to open a GitHub issue to track that follow-up work?


🧠 Learnings used
Learnt from: auxesis
Repo: cipherstash/stack PR: 431
File: .github/workflows/tests-bench.yml:0-0
Timestamp: 2026-05-06T11:35:47.341Z
Learning: Do not use continue-on-error: true in GitHub Actions workflows to mask test failures. Masking failures hides root causes and makes diagnosis harder. Keep failing steps blocking so genuine issues surface. If a test is known to fail or flaky, isolate it in a separate workflow or job (e.g., a dedicated flaky-tests job) or mark it as flaky in the test suite with clear reporting, rather than masking in the main workflow. Apply this rule to all YAML workflow files under .github/workflows, regardless of PR descriptions.


- uses: pnpm/action-setup@v6
name: Install pnpm
with:
run_install: false
# Do not use caching in this cache-testing workflow
cache: false

- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: 22
# Do not use caching in this cache-testing workflow
package-manager-cache: false

# node-pty's install hook falls back to `node-gyp rebuild` when no
# linux-x64 prebuild matches. pnpm/action-setup v6 no longer ships
# node-gyp on PATH, so install it explicitly.
- name: Install node-gyp
run: npm install -g node-gyp

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run lint script self-tests
run: pnpm run test:scripts

- name: Verify no caching in release.yml and tests-supply-chain.yml
run: pnpm run lint:workflow-cache
59 changes: 41 additions & 18 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Security Policy

CipherStash takes the security of our software, infrastructure, and customers extremely seriously.
CipherStash takes the security of our software, infrastructure, and customers extremely seriously.
This document describes the security posture, reporting process, and guidelines for this repository and associated packages.

## Supported Packages
Expand Down Expand Up @@ -80,13 +80,13 @@ We will acknowledge receipt within **48 hours** and provide regular updates unti

CipherStash follows a **coordinated responsible disclosure** process:

1. **Submit report** privately via `security@cipherstash.com`.
2. **Acknowledgement** within 48 hours.
3. **Assessment** of severity using CVSS and internal risk models.
4. **Fix development** and patch release in a private branch.
1. **Submit report** privately via `security@cipherstash.com`.
2. **Acknowledgement** within 48 hours.
3. **Assessment** of severity using CVSS and internal risk models.
4. **Fix development** and patch release in a private branch.
5. **Coordinated disclosure**, including:
- New patch release(s)
- Security advisory on GitHub
- Security advisory on GitHub
- Credit to reporter (optional)

We will never take legal action against good-faith security researchers who follow this policy.
Expand All @@ -102,14 +102,14 @@ The following are **in scope**:
- Protect.js cryptographic implementations, configuration layers, and CLI tooling
- Key-handling, authenticated encryption behaviour, JSON/JSONB field-level encryption flows
- Documentation or code examples that could lead to insecure usage
- CipherStash’s internal infrastructure
- CipherStash’s internal infrastructure
- CipherStash Proxy, ZeroKMS, or other backend products

The following are **out of scope**:

- Example applications in the `examples` dir (though we are still grateful for any relevant disclosires there)
- Social engineering, physical attacks, or denial-of-service
- Attacks requiring privileged access to developer machines or CI/CD infrastructure
- Social engineering, physical attacks, or denial-of-service
- Attacks requiring privileged access to developer machines or CI/CD infrastructure

---

Expand All @@ -118,23 +118,46 @@ The following are **out of scope**:
To maintain a strong security posture, contributors MUST:

### ⚙️ Follow cryptographic safety rules
- Do **not** modify cryptographic primitives without prior discussion
- Avoid introducing new crypto dependencies without prior discussion
- Never check in test keys, secrets, or example credentials
- Do **not** modify cryptographic primitives without prior discussion
- Avoid introducing new crypto dependencies without prior discussion
- Never check in test keys, secrets, or example credentials

### 🛡 Coding & dependency hygiene
- Avoid adding dependencies unless necessary
- Keep dependencies updated and vetted
- Use TypeScript for all new code
- Ensure all code paths that handle keys or encrypted data include type-safe boundaries
- Avoid adding dependencies unless necessary
- Keep dependencies updated and vetted
- Use TypeScript for all new code
- Ensure all code paths that handle keys or encrypted data include type-safe boundaries

### 🔍 Testing & review
- Submit PRs with tests covering edge cases and misuse-resistant behaviour
- Flag any changes involving key derivation, key wrapping, AAD, or encryption modes for mandatory security review
- Submit PRs with tests covering edge cases and misuse-resistant behaviour
- Flag any changes involving key derivation, key wrapping, AAD, or encryption modes for mandatory security review
- Do not merge PRs that downgrade security controls or introduce unsafe defaults

---

## CI/CD Supply-Chain Hardening

The `release.yml` workflow publishes packages to npm using OIDC trusted
publishing and an `NPM_TOKEN`.

[GitHub Actions cache poisoning is a known attack][1] against credential-bearing
workflows. The mechanism is:

- A lower-privileged workflow run plants a malicious entry under a deterministic
cache key
- A privileged workflow restores a cache from that deterministic cache key
- The malicious entry is executed by the privileged workflow, and secrets
are exfiltrated

We mitigate this by:

- Explicitly disabling all caching in `release.yml`
- Automated checks for disabled caching on high-risk workflows

[1]: https://adnanthekhan.com/2024/05/06/the-monsters-in-your-build-cache-github-actions-cache-poisoning/

---

## Questions?

For general questions about CipherStash security practices (not security incidents), contact:
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"clean": "rimraf --glob **/.next **/.turbo **/dist **/node_modules",
"code:fix": "biome check --write",
"lint:runners": "node scripts/lint-no-hardcoded-runners.mjs",
"lint:workflow-cache": "node scripts/lint-no-workflow-caching.mjs",
"release": "pnpm run build && changeset publish",
"test": "turbo test --filter './packages/*'",
"test:e2e": "turbo run test:e2e",
Expand All @@ -57,6 +58,7 @@
"@biomejs/biome": "^1.9.4",
"@changesets/cli": "^2.29.6",
"@types/node": "^22.15.12",
"js-yaml": "^3.14.2",
"rimraf": "^6.1.2",
"turbo": "2.1.1",
"vitest": "catalog:repo"
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Actions Cache Restore
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Restore build cache
uses: actions/cache/restore@v4
with:
path: .turbo
key: turbo-${{ github.sha }}
- run: pnpm install --frozen-lockfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Actions Cache Save
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Save build cache
uses: actions/cache/save@v4
with:
path: .turbo
key: turbo-${{ github.sha }}
- run: pnpm install --frozen-lockfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Actions Cache
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Restore build cache
uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ github.sha }}
- run: pnpm install --frozen-lockfile
19 changes: 19 additions & 0 deletions scripts/__tests__/fixtures/lint-no-workflow-caching/clean.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Clean
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
with:
run_install: false
cache: false
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: 22
package-manager-cache: false
- run: pnpm install --frozen-lockfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Missing Explicit False
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: 22
- run: pnpm install --frozen-lockfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: No Actions Cache
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
# `actions/cache` is named here in prose but never used as a step —
# the check must only match a real `uses:` reference.
- name: Note — actions/cache is intentionally avoided
run: echo "this project does not use actions/cache"
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Setup Node Cache
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
Loading
Loading