security(spec-sync): harden against upstream compromise#109
Conversation
Implement Option (c) from issue #92 / audit finding F-O-01: drop the writable auto-PR flow entirely. The weekly spec-sync workflow now runs with the narrowest possible scope and never mutates the repo. Mitigations applied: - Reduced `permissions:` from `contents: write` + `pull-requests: write` to `contents: read` + `issues: write`. The workflow can no longer push a branch, open a PR, or write to the working tree of any ref. - Removed the `peter-evans/create-pull-request` step. A compromise of upstream `docs.kalshi.com` can no longer reach a writable branch via this workflow. - Removed the in-CI `generate.py` / `ruff` / `mypy` / `pytest` steps. Those executed Python derived from attacker-controllable upstream YAML inside CI runners; maintainers now re-generate and test locally before opening a PR by hand. - Drift is reported as a comment on a tracking issue (#92), including sha256 checksums of the fetched upstream specs so the maintainer can reproduce and verify the same content locally before generating models. - Existing SHA pinning of third-party actions (`actions/checkout`, `astral-sh/setup-uv`) is preserved; the SHA-pinning principle is extended to the *content* being merged: nothing reaches the repo without a human in the loop. Closes #92 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #109: security(spec-sync): harden against upstream compromiseOverviewThis PR implements Option (c) from issue #92, closing the supply-chain attack surface in the spec-sync workflow. The threat was real and the fix is surgical: strip write permissions, stop auto-executing upstream-controlled content in CI, and shift regeneration to a manual, human-audited step. The approach is sound. Security — CorrectPermissions reduction is the right call. Removal of Env-var injection pattern is done correctly. All Heredoc safety — the SHA-pinning on third-party actions is preserved. Good. One Issue Worth Fixing Before Merge
The PR footer says Recommendation: before merging, open a new long-lived tracking issue (e.g., "Spec-sync drift tracking") and update the env var to point to it. Or, if the intent is to reuse #92 as a running log, keep it open and remove it from Minor Observations
Summary
Fix the tracking issue target and this is good to merge. |
Per code review on #109: the workflow's SPEC_SYNC_TRACKING_ISSUE was pointed at #92, which #109 closes. Drift comments would land on a closed issue. Switched to #113 (newly opened "Spec-sync drift tracking") so the running log lives separately from the security-hardening work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #109: security(spec-sync): harden against upstream compromiseOverviewThis PR implements Option (c) from issue #92 (F-O-01 audit finding): the spec-sync workflow is stripped of all write permissions and can no longer auto-generate code from upstream-controlled YAML or open PRs. Drift is now reported as an issue comment on a long-lived tracking issue (#113), and a maintainer manually runs the regen pipeline. This is the correct approach for a high-severity supply-chain risk. What's Good
Issues and Suggestions1. Hardcoded tracking issue number — fragile, no URL env:
SPEC_SYNC_TRACKING_ISSUE: "113"If issue #113 is ever closed, locked, or a new tracking issue is opened, someone must remember to update this. A comment with the full issue URL would help maintainers find it quickly: env:
# https://github.com/TexasCoding/kalshi-python-sdk/issues/113
SPEC_SYNC_TRACKING_ISSUE: "113"Low severity — but tracking issues do get closed/replaced. 2. The report body uses an unquoted heredoc: body=$(cat <<EOF
## Weekly spec sync drift report
...
\`contents: read\` only
...
${OPENAPI_CHANGED}
EOF
)The unquoted form is required here because you need Minor note: the backtick escaping ( 3. Silent drift if With Consider adding a 4. Removed informative comment about SHA-pinning rationale The old comment: …was removed along with the old permission set. The new top-level security block mentions SHA pinning but doesn't explain the mutable-tag threat. The new version is still an improvement overall, but consider adding a one-liner to the checkout step explaining why it's pinned: # Pinned to SHA — a compromised mutable tag would still run with issues:write
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2Minor documentation nit. Summary
This is a well-executed mitigation of a real supply-chain risk. The two |
Replaces the long-lived #113 comment-stream pattern with per-drift issues so each drift has its own lifecycle (assignable, labelable, auto-closed by regen PR via "Closes #N"). - Drift fingerprint = sha256(openapi_sha || asyncapi_sha). Embedded in issue body as "<!-- fingerprint:HEX -->". - Before opening, workflow searches open spec-drift-labeled issues for the fingerprint. If found, no-op (re-runs against unchanged upstream are silent; weekly cron won't spam). - Removed SPEC_SYNC_TRACKING_ISSUE env var and the hardcoded #113 coupling the bot review flagged. - Title pattern: "Spec drift YYYY-MM-DD: openapi vX → vY". - Body now includes "Closes #<this issue number>" guidance so the regen-PR lifecycle is obvious to maintainers. Permission scope unchanged (issues: write is sufficient for create). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Reworked per second-pass bot feedback + user direction to "auto-open new issues on spec drift when they arise." New design: workflow opens one This eliminates every concern from the bot's review pass:
#113 closed as superseded. PR body updated. Ready for re-review. |
Code Review — PR #109: security(spec-sync): harden against upstream compromiseOverviewThis PR closes the supply chain attack surface in the spec-sync workflow (issue #92). Previously, the workflow held Strengths
IssuesMedium — Heredoc expands env vars; malicious spec path could trigger command substitutionbody=$(cat <<EOF
...
${ADDED_PATHS}
...
EOF
)
The fix is low-disruption: switch to Low — Dedup search capped at 100 issuesgh issue list --limit 100 --label spec-driftIf more than 100 Low —
|
Per third-pass bot review on #109 (Medium finding): the prior unquoted heredoc (cat <<EOF ... EOF) expanded both ${VAR} and $(cmd) inside body construction. The substituted values include ADDED_PATHS / REMOVED_PATHS etc., which are sourced from upstream YAML. While OpenAPI paths are normally constrained, if upstream ever contained "$(rm -rf /)" the heredoc would execute it during body assembly. This was the one remaining code-execution path from upstream content — closes that gap. - New scripts/render_drift_body.py: uses string.Template.substitute, which performs literal string substitution only (no shell, no re-interpretation of values). Reads all placeholders from os.environ; the workflow sets them via env:. - Workflow body construction collapses to: body_file=$(mktemp) python3 scripts/render_drift_body.py > "$body_file" gh issue create ... --body-file "$body_file" - Switching to --body-file also addresses the Low finding about argv length limits on large diffs. - Bumped dedup --limit from 100 to 200 (other Low finding). Smoke-tested locally with adversarial ADDED_PATHS='/v2/markets/$(rm -rf /)': the payload renders as a literal string in the output, no execution. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Third-pass addressed: Fixed
Acknowledged, not changed
|
Code Review — PR #109: security(spec-sync): harden against upstream compromiseOverviewThis PR closes supply-chain risk #92 by stripping the spec-sync workflow of write permissions and removing all CI steps that executed upstream-derived Python. Drift is now surfaced via a labeled, deduplicated GitHub issue; maintainers regenerate locally and open a PR by hand. The design is sound and the implementation is careful. Positives worth calling out
Issues1.
|
Per fourth-pass bot review on #109: - Added "Ensure spec-drift label exists" step that runs gh label create --force before the open step. Idempotent. Removes the silent-fail footgun where, if the label were ever deleted, weekly cron drift would go unreported because gh issue create --label aborts under set -euo pipefail. - render_drift_body.py catches KeyError from Template.substitute and sys.exit()s with a one-line message identifying the missing var. No more bare Python traceback in CI logs if an upstream step output is unexpectedly empty. - Dropped unused "from __future__ import annotations" (no annotations). - Added "trap 'rm -f $body_file' EXIT" to clean up the rendered body on step exit. Ephemeral runners make this cosmetic, but idiomatic. Tested both render paths locally: happy path writes the substituted body; missing env var exits 1 with the var name in the message. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Fourth-pass addressed: Fixed
Skipped
|
Code Review — PR #109: security(spec-sync): harden against upstream compromiseOverviewThis PR addresses issue #92 (audit finding F-O-01): the old workflow had a path from upstream-controlled YAML into executed CI code (generate.py, ruff, mypy, pytest all ran on content derived from attacker-controllable specs). The fix is well-scoped and the design rationale is clearly documented. Overall this is solid security work. What's done well
Issues and suggestionsMedium — Residual execution surface:
|
Per bot review on PR #151: - CODE_OF_CONDUCT.md: drop the suggestion to route CoC reports through GitHub's Private Vulnerability Reporting. PVR is wired into the security-advisory tooling (CVSS, advisory drafts, security alerts) and conflating CoC + vuln channels confuses both reporters and maintainers. Point at direct GitHub-profile contact instead; reserve PVR for actual vulnerabilities per SECURITY.md. - CONTRIBUTING.md: `uv sync --dev` → `uv sync` to match CLAUDE.md (uv installs dev-group deps by default; --dev is at best a no-op). Verified the SECURITY.md claim about spec-sync permissions is accurate: `.github/workflows/spec-sync.yml` permissions block reads `contents: read + issues: write`, locked in by PR #109 (Wave 1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) * chore: OSS hardening — community files, gitignore, scratch cleanup Done in tandem with repo-level setting changes applied via `gh`: - branch protection on main (required status checks: test 3.12 / 3.13 / drift-check; linear history; no force-push; no branch deletions) - Dependabot security updates + vulnerability alerts enabled - private vulnerability reporting enabled - wiki disabled, Discussions enabled This commit adds the on-repo side: Community files (lifts GitHub community-profile score 42% → 100%): - SECURITY.md — disclosure policy, supported versions, scope, in-tree security measures. - CODE_OF_CONDUCT.md — points at Contributor Covenant v2.1 by canonical URL with project-specific reporting channel. - CONTRIBUTING.md — dev setup, conventions, PR checklist; mirrors CLAUDE.md. - .github/ISSUE_TEMPLATE/{bug_report,feature_request,config}.yml — structured forms; config.yml redirects security to Private Vulnerability Reporting and questions to Discussions. - .github/PULL_REQUEST_TEMPLATE.md — standard PR scaffold. Cleanup: - Deleted .planning/ (6 audit reports from the v2.0 hardening wave; preserved in git history, no longer needed in main). - Deleted scripts/audit_demo_feasibility.py (one-off v0.10–v0.13 endpoint feasibility probe; not part of the supported tooling). - Purged 92 .DS_Store files from the working tree. .gitignore additions: **/.DS_Store, .planning/, .claude/worktrees/, .venv.stale*. Keeps the patterns above out of future commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * review(#151): split CoC reporting from security channel + align uv sync Per bot review on PR #151: - CODE_OF_CONDUCT.md: drop the suggestion to route CoC reports through GitHub's Private Vulnerability Reporting. PVR is wired into the security-advisory tooling (CVSS, advisory drafts, security alerts) and conflating CoC + vuln channels confuses both reporters and maintainers. Point at direct GitHub-profile contact instead; reserve PVR for actual vulnerabilities per SECURITY.md. - CONTRIBUTING.md: `uv sync --dev` → `uv sync` to match CLAUDE.md (uv installs dev-group deps by default; --dev is at best a no-op). Verified the SECURITY.md claim about spec-sync permissions is accurate: `.github/workflows/spec-sync.yml` permissions block reads `contents: read + issues: write`, locked in by PR #109 (Wave 1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * review(#151): SHA-pin release.yml + soften claude-review wording Per second bot review on PR #151: - release.yml: SHA-pin all third-party actions (actions/checkout@v4.2.2, astral-sh/setup-uv@v6.3.1, actions/upload-artifact@v4.6.2, actions/download-artifact@v4.3.0, pypa/gh-action-pypi-publish@v1.14.0, softprops/action-gh-release@v2.2.1). This matches what SECURITY.md already claims about release-path workflows being SHA-pinned. The bot was right — claim was overstated for release.yml (claude.yml, claude-code-review.yml, spec-sync.yml were already pinned). - CONTRIBUTING.md: soften claude-review wording. It's advisory, not a required check, and fails by design on Dependabot / workflow-self- modifying PRs. New wording reflects that. - PR template: wrap bare `Closes #` placeholder in an HTML comment so GitHub's issue-link parser doesn't see a malformed reference, and add a note that the section can be deleted if no issue is referenced. Verified pip-audit.yml exists (third claim from bot was correct to spot-check). All three SECURITY.md claims now match reality. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Spec-sync workflow no longer has a path from upstream-controlled YAML into committed/PR'd repo content. Mitigations (Option (c) from #92):
permissions:reduced fromcontents: write+pull-requests: writetocontents: read+issues: write. Workflow can no longer push branches or open PRs.peter-evans/create-pull-requeststep. No automated branch/PR creation from upstream content.generate.py/ruff/mypy/pyteststeps. Those previously executed Python derived from attacker-controllable upstream YAML; maintainers now regenerate and test locally before opening a PR by hand.spec-drift-labeled issue, one per distinct fingerprint — sha256 of openapi_sha + asyncapi_sha, embedded in the issue body as<!-- fingerprint:HEX -->. Re-runs against unchanged upstream are silent; if drift recurs, the existing open issue dedups so weekly cron won't spam. Each drift gets its own lifecycle (assignable, labelable, auto-closed byCloses #Nin the regen PR).actions/checkoutandastral-sh/setup-uv.Design pivot from earlier draft
The first version of this PR posted comments on a long-lived
#113tracking issue. After bot review feedback ("Hardcoded tracking issue number — fragile, no URL") and user feedback ("can we automatically open new issues on spec drift?"), reworked to the per-drift pattern above. #113 is now closed as superseded.Not addressed (upstream-dependent, per #92)
Closes #92
Test plan
yaml.safe_loadspec-drift-labeled issueCloses #Nauto-closes the drift issue on merge