release: promote main to prod — publishes 0.10.6 - #270
Merged
Merged
Conversation
…e API key (rf-7xv0, rf-v2mj) P0, authorized by Rome 2026-09-13. Both defects are live AT THE v1 TAG, which is what consumers pin. MERGING THIS WITHOUT MOVING v1 CHANGES NOTHING FOR ANY CONSUMER — v1 is 34850d4, 115 commits and five months behind main, so no fix that has ever landed on main has reached them. rf-7xv0 — KEY EXFILTRATION, github-action/action.yml. "branch_name": "${{ github.head_ref || github.ref_name }}", sat inside a `run:` block whose env carries RAFTER_API_KEY. The runner expands `${{ }}` into the script TEXT before bash sees it, so that value was not data — it was source code, chosen by whoever opened the pull request, and anyone can open one from a fork. Measured, not argued: with a branch name of x"; printf %s "$RAFTER_API_KEY" > "$CANARY_PATH"; echo " the canary file came back containing the key. `$( )` works too and needs no quote-breaking at all. Fixed the way GitHub documents: the values reach the script through `env:` (GH_REPOSITORY, GH_BRANCH) and never through `${{ }}` in `run:`. The body is then built with `jq -nc --arg` rather than pasted into a hand-quoted JSON string, which fixes a second bug in the same line — a branch name containing a quote or backslash produced MALFORMED JSON even with no attacker involved. Hand-quoting would have to get shell and JSON escaping both right; jq --arg gets both right by construction. rf-v2mj — AN UNREADABLE REPORT RENDERED AS A CLEAN SCAN, root action.yml. `COUNT=$(... | jq ... || echo "0")` made "no findings" and "I could not read the output" the same value, so the gate passed precisely when it could not see its input. Now the count is written only when a parse actually succeeded; otherwise the step fails and finding-count is left EMPTY rather than a fabricated 0. The text branch distinguishes grep's exit 1 (no matches — clean) from grep failing (>1). Note which file that is: the same defect class in github-action/action.yml was fixed under sable-fgk7 and this ROOT file was never touched by it. Two action.yml files; only one had been fixed. That is the measurement trap the bead warned about, and it is why the corpus is stated below. PROBES — each FAILS on the pre-fix file and passes after, each with a control so it cannot pass by doing nothing: test-trigger-injection.sh unfixed 3 failures -> fixed 0 quote-breaking injection reads the key; $( ) injection reads the key; and a FIDELITY control that a legal branch name containing a quote and a backslash still arrives intact as valid JSON, checked against a real local listener that records the body actually sent. test-root-action-counts.sh unfixed 2 failures -> fixed 0 unparseable report and truncated JSON must fail the step; CONTROLS that a genuinely clean report still passes with count 0 and that real findings are still counted as 2. Without those controls a "fail on everything" change would have passed. Writing the fidelity control honestly cost two rewrites: the first scraped the script text for the old inline-JSON shape and silently matched nothing once the fix removed that shape, and the probe did not model the step's `env:` block, so after the fix the payload was not arriving anywhere and the injection tests were passing vacuously. Both are why the probe now renders the env mapping the way the runner does. CI: both probes wired into test-github-action.yml. The workflow's path filter watched only `github-action/**`, so a change to the ROOT action.yml would not have run the probe that guards it; `action.yml` added to both the pull_request and push filters. CORPUS SEARCHED, as the bead requires. Every action file at both refs: origin/main action.yml head_ref/ref_name: 0 origin/main github-action/action.yml head_ref/ref_name: 1 v1 action.yml head_ref/ref_name: 0 v1 github-action/action.yml head_ref/ref_name: 1 plus .github/workflows/test-action.yml and test-github-action.yml, which are workflows rather than actions and carry no such interpolation. NOT FIXED HERE, reported rather than silently left. A sweep of every `${{ }}` inside a `run:` block found more of the same CLASS, none of them attacker-controlled from a fork: root action.yml interpolates inputs.scan-path, inputs.args, inputs.format and inputs.version directly into run blocks, so a consumer whose workflow passes untrusted text into those inputs has the same shape of problem one level out. github-action/action.yml also interpolates github.repository and github.event.pull_request.number, both runner-supplied and narrowly typed. Worth its own pass; not smuggled into a P0.
Closes sable-oubg (rf-7xv0 key exfiltration + rf-v2mj unreadable-report-as-clean). Why the earlier fix missed this: sable-fgk7 repaired the same defect class in github-action/action.yml in #224 and never opened the ROOT action.yml. Two files with the same name, one fixed, for three weeks. The CI path-filter change in this PR is what stops that recurring — a workflow that watched only github-action/** could never have guarded the root file. The v1 tag moves to this merge commit; merging alone protects no consumer.
…repo parse_remote/parseRemote took the last two path segments of ANY git remote with no host check, so a non-GitHub-shaped remote silently produced a wrong slug: an Azure DevOps remote (.../org/proj/_git/repo) became "_git/repo", and a bare filesystem remote became "<parent-dir>/<repo>". The backend turns that into https://github.com/{slug} and 404s, burning a paid scan every time. Both runtimes now require a recognized host (GitHub, GitLab, Bitbucket, Gitea -- the existing multi-provider set) and raise a clear error naming the remote otherwise. safe_branch/safeBranch had the same shape of bug: on a detached HEAD it fell back to a short commit SHA, and on total git failure to a hardcoded "main" -- submitting either as a branch name is a guaranteed branch-not-found failure, and the hardcoded default is also just a guess that can be wrong. Both now raise instead of fabricating a value. Table-driven tests over real remote shapes (github https/ssh/.git, gitlab, azure devops, a bare filesystem path) and a detached HEAD, in both runtimes; each new/changed assertion was confirmed to fail against the pre-fix code before the fix landed. sable-pqmw
…kup crash Security review of the parent commit found three real problems in the new host check: - A naive ":" -> "/" substitution treated the userinfo separator in "https://user:token@host/..." the same as the SCP host:path separator, so the value checked against the host allowlist could be attacker-chosen credentials rather than the real host. Concretely, "https://github.com:x@evil.com/foo/bar" parsed as host "github.com" (allowed) while the real host, evil.com, was silently discarded -- a bypass of the check this fix exists to add. - The same bug hard-fails legitimate credentialed HTTPS remotes (PAT-embedded clone URLs, common in CI) that used to at least produce a slug. - An explicit "ssh://" scheme was never stripped, so a normal "ssh://git@github.com/owner/repo.git" remote -- not an adversarial shape -- was rejected as host "ssh". _split_remote/splitRemote now use the URL parser (urlsplit / URL) for scheme-based remotes instead of a blanket colon substitution, so userinfo and port are stripped the same way a browser or curl would strip them, and "ssh://" is recognized alongside "https?://". Also: the new RuntimeError messages embed the remote URL/host, which is attacker-influenceable (a malicious repo's own remote config). Two call sites (issues create from-scan/from-text) render caught errors through Rich markup; a value containing something like "[/bold]" closed a tag that was never opened and crashed with an uncaught MarkupError instead of a clean error + exit code. Escaped before rendering. (Node has no equivalent -- its error rendering is a plain template literal, not a markup language.) Each new/changed test was confirmed to fail against the pre-fix code first. sable-pqmw
…t-validation Reject unrecognized git remote hosts; fix safe_branch fallback
The Rafter Sites entry cited a private repo and PR number, and described the internal endpoint surface it calls. This is a public changelog.
Typer's pretty tracebacks print every frame's locals by default. Request helpers keep credentials in locals, so an unhandled exception could echo them to stderr. Turn show_locals off on the root app. Adds a subprocess test that forces a transport failure and asserts a sentinel credential never reaches stdout or stderr.
The working directory can be an untrusted repository, so its .env must not supply operator settings such as the API key, GitHub token, notify webhook or paid-scan confirmation. Node: the startup dotenv guard now drops every RAFTER_* variable that .env introduced, not only the disable and hook switches. Values already in the real environment are untouched. Python: resolve_key no longer calls load_dotenv(). Its upward search starts from the install path, which reaches a repository's .env when the virtualenv lives inside it. Docs no longer suggest putting the API key in .env; use the environment or the global config file.
`rafter run` scans the remote repository, but when it auto-detected the current local branch it never checked that the branch had been pushed. The scan was queued and then failed on the backend minutes later. When both repo and branch come from the local checkout, ask origin with `git ls-remote --exit-code --heads`. If the remote answers without the branch, exit 1 with a message to push it or pass --branch. If origin cannot be reached, proceed as before. If the pushed commit differs from local HEAD, note that the scan covers the pushed commit. Explicit --branch and CI-provided branches are unchanged. Same behavior in the Node and Python CLIs, each with a test against a real local bare remote.
The --diff value is passed to `git diff` as a positional argument, and git parses anything starting with "-" as one of its own options. Reject such a value with exit 2 (invalid ref) before running git, in both the Node and Python CLIs, for `rafter secrets` and `rafter agent scan`. Each runtime gets an end-to-end test that passes an option-shaped ref and asserts exit 2 with the target file left untouched.
`agent init --with-gemini` built `gemini skills link "<path>"` as a shell string, quoting the path with JSON.stringify. Double quotes do not stop a POSIX shell from expanding $(...) or backticks, and the path comes from the working directory. Run gemini with an argument array instead; Windows keeps cmd.exe, which it needs for gemini's .cmd shim and which treats a double-quoted path literally. Same change for two siblings rooted in the home directory: the global `git config core.hooksPath` call in install-hook and the local betterleaks version probe in status. Adds an end-to-end test that runs init from a directory whose name contains shell syntax, with a stub gemini on PATH, and asserts the path arrives literally and nothing is executed.
fix(python): do not render local variables in tracebacks
…tial-guard fix: never take RAFTER_* settings from a project .env
…-remote fix(run): refuse an auto-detected branch that is not on the remote
…on-guard fix(secrets): reject a --diff ref that starts with "-"
…o-shell fix(node): pass paths to child processes as arguments, not shell strings
Bump node, python and both ClawHub skill manifests 0.10.5 -> 0.10.6, and record what this release ships. 0.10.6 carries five fixes already on main: - Unhandled errors no longer print the API key in a traceback (#264) - A project .env can no longer supply RAFTER_* settings, including the API key (#265) — behavior change, noted in the CHANGELOG - rafter run now checks that an auto-detected branch has been pushed before scanning it (#266) - rafter secrets --diff / rafter agent scan --diff reject an option-shaped ref before it reaches git (#267) - rafter agent init --local --with-gemini no longer runs gemini through a shell (#268) Verified via scripts/check-version-unpublished.sh that 0.10.6 is not yet on npm or PyPI.
release: v0.10.6 — version bump so the release gate can pass
Raftersecurity
approved these changes
Oct 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is the promote PR: merging it publishes 0.10.6 to npm and PyPI. It tracks
main, so it will pick up the version bump automatically once #269 merges, andvalidate-releasewill go green at that point. Until then it is red for one reason only:mainstill says0.10.5and0.10.5is already on both registries, which is #256's gate working as designed.Order: approve #269 → it merges to
main→ this goes green → approve this.What merging this publishes
Five fixes, all already on
main:.envcan no longer supply Rafter's own settings (fix: never take RAFTER_* settings from a project .env #265). Previously a.envin the working directory could overrideRAFTER_API_KEYand otherRAFTER_*settings you configured yourself. Behavior change: if you relied onRAFTER_API_KEYin a project.env, move it to your shell environment or~/.rafter/config.json— the CLI will now report the key as missing if.envwas its only source.rafter runnow checks that an auto-detected branch has been pushed before scanning it (fix(run): refuse an auto-detected branch that is not on the remote #266). Running without--branchfrom a branch that doesn't exist on the remote used to queue a scan that failed later with a "branch not found" error; it now fails fast with a clear message.rafter secrets --diff/rafter agent scan --diffreject an option-shaped ref before it reaches git (fix(secrets): reject a --diff ref that starts with "-" #267). A--diffvalue starting with-could previously be misread by git as an option rather than a ref.rafter agent init --local --with-geminino longer runs through a shell (fix(node): pass paths to child processes as arguments, not shell strings #268). A path containing shell metacharacters could previously have part of it executed during skill registration.After merging
Check the registry, not the job:
npm view @rafter-security/cli versionmust read0.10.6.