Skip to content

fix(source-control): close five pre-existing secret and hold-path detection gaps (hardening deferred) #10943

Closed as not planned

Description

@kyle-sexton

An independent security review of #10743's last round (c44bfb6) found no HIGH/CRITICAL and no regression. It found five gaps that predate that commit; #10743 does not widen them. Fix them after #10743 merges, because they touch the same files.

Findings

  1. MEDIUM: a quoted inherit escapes both secret checks. babysit_risk.py _NON_JOB_SECRET and _SECRET_REF match \bsecrets\s*:\s*inherit\b. secrets: "inherit", secrets: 'inherit' and 'secrets': inherit return gated=False, and no secret-exposure-review fires. Scenario: a PR adds uses: other/repo/.github/workflows/x.yml@sha with secrets: "inherit" to a workflow outside the gate, and every repository secret reaches outside code after merge. Fix: \bsecrets['"]?\s*:\s*['"]?inherit\b in both, plus a test per quoted form. That GitHub parses a quoted inherit like the bare word is UNVERIFIED; YAML treats them as the same string.
  2. LOW: the comment strip cuts at # inside quoted strings. line.split(" #", 1)[0] in gated_workflow and _code_lines. run: echo ' #' ${{ secrets.X }} is not gated. The patch-line check still holds it locally, but merge-holds.mjs reads only the generated GATE_WORKFLOWS table. Fix: strip only outside quotes and ${{ }}, or scan YAML scalars.
  3. LOW: a quoted id-token key escapes _MINTED_CREDENTIAL. "id-token": write is not gated. Fix: \b['"]?id-token['"]?\s*:\s*['"]?write\b.
  4. LOW: a symlink named .claude or .claude-lane escapes the mjs and bash matchers. Python's glob_match holds the bare directory name, but merge-holds.mjs matchesHoldPath and the hold_patterns in pr-require-checks.yml do not. A symlink plugins/x/.claude -> ../../docs/payload plus files under docs/payload/ passes the CI holds; only the local babysit lane (symlink = HIGH) catches it. Fix: match the directory name itself for X/** globs in mjs and bash, or hold any mode-120000 entry in both. Also make the matcher test assert all three are equal, not only that Python holds at least what Node holds.
  5. LOW: a pr-review-main: <lane> record from any workflow passes the app-slug check. A workflow with checks: write on a same-repo branch can post as the github-actions app. A forged clean record cannot lower a real CRITICAL (the max count wins, and a counted verdict from a successful main run is required), so the impact is a forged CRITICAL holding a PR. Optional hardening: bind the record to the run that uploaded the verdict.

Acceptance

  • Each form above has a failing-then-passing test in test_babysit_risk.py / test_merge_holds_sync.py.
  • scripts/sync-hold-floor.py --check stays green.

Refs: #10743

Activity

  1. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 29, 2026
  2. github-actions commented on Oct 29, 2026

    @github-actions
    Contributor

    Proposed work class: work-class: scoped
    Basis: automated intake triage; the lanes App applied this class.

    Size: Medium
    Complexity: Medium
    Uncertainty: Known
    Flags: none
    Blocked by: none

    Understood: five pre-existing detection gaps in the babysit risk checks and merge-hold matchers. They are a quoted inherit and quoted id-token key, comment stripping inside quoted strings, symlinked .claude directories, and an unbound pr-review-main record. Each comes with a proposed fix, and acceptance is a failing-then-passing test per form with sync-hold-floor.py --check staying green. The issue says to start after #10743 merges. That is a PR, not an open issue, so I listed no blocker. No likely duplicates found among open issues.

    Automated triage, applied by the lanes App.

  3. added
    priority: mediumReal value, no hard deadline; normal backlog flow.
    and removed
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 29, 2026
  4. added
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.
    status: confirmedA maintainer confirmed the item is real and in scope; ready to be worked.
    and removed
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    on Oct 29, 2026
  5. melodic-automation-lanes commented on Oct 29, 2026

    @melodic-automation-lanes
    Contributor

    Not admitted to the issue lane: no trusted-human acceptance criteria: a listed human must put the confirmed - [ ] criteria in the body or post them as their own comment, or, on an issue a listed human filed, intake's newest refinement comment must hold them unedited. Admission takes the issue once each of those is met, when intake triages it again (.github/actions/resolve-config/admission-rules.json).

  6. melodic-automation-lanes commented on Oct 29, 2026

    @melodic-automation-lanes
    Contributor

    The latest triage verdict holds nothing: admission moved the issue to status: confirmed; its comment says why.

  7. melodic-automation-lanes commented on Oct 29, 2026

    @melodic-automation-lanes
    Contributor

    Refinement. A read-only run explored this repository, searched its issues and pull requests, and read the allowed docs to draft what follows. On an issue a listed human filed, it counts as the issue's spec once posted, while unedited; it is still model text, so check it.

    Intended outcome: Close the five detection gaps (quoted inherit, quote-aware comment strip, quoted id-token, symlinked .claude dirs, unbound pr-review-main record) with a failing-then-passing test each, after #​10743 merges.

    Dependencies

    • #​10743 must merge first: the issue says its fixes touch the same files, and the checkout has no _NON_JOB_SECRET, _MINTED_CREDENTIAL or gated_workflow yet

    Related work

    Open questions

    1. Is #​10743 merged? The checkout at .base has only _SECRET_REF, with no _NON_JOB_SECRET, _MINTED_CREDENTIAL or gated_workflow, so findings 1-3 cannot be located as written until it lands.
      RECOMMENDED: Hold the issue until #​10743 merges, then re-run refinement against the merged base. Basis: .base/plugins/source-control/skills/babysit-prs/scripts/babysit_risk.py:908
    2. For finding 4, should the fix match the bare directory name for X/** globs in mjs and bash, or hold any mode-120000 entry in both?
      RECOMMENDED: Match the bare directory name for X/** globs in both, so all three matchers agree with Python glob_match and the equality test can assert it. Basis: .base/.github/scripts/pr-merge/merge-holds.mjs:245
    3. Is finding 5 (binding the pr-review-main record to the uploading run) in scope? The issue calls it optional hardening.
      RECOMMENDED: Leave it out of this issue and file it separately, since it has no stated acceptance test. Basis: judgment
    4. For finding 2, strip comments outside quotes and ${{ }} or scan YAML scalars?
      RECOMMENDED: Use a quote- and expression-aware strip, which is smaller than a YAML scan and stays dependency-free. Basis: .base/plugins/source-control/skills/babysit-prs/scripts/babysit_risk.py:955
    5. Does GitHub parse a quoted secrets: "inherit" like the bare word? The issue marks this UNVERIFIED.
      RECOMMENDED: Treat it as inherit and gate it. Over-holding is safe, and YAML gives the same string. Basis: judgment

    Sources read

    • /home/runner/work/claude-code-plugins/claude-code-plugins/.base/plugins/source-control/skills/babysit-prs/scripts/babysit_risk.py:908: _SECRET_REF matches the bare secrets: inherit form
    • /home/runner/work/claude-code-plugins/claude-code-plugins/.base/plugins/source-control/skills/babysit-prs/scripts/babysit_risk.py:955: comment strip uses line.split(" #", 1)[0]
    • /home/runner/work/claude-code-plugins/claude-code-plugins/.base/.github/scripts/pr-merge/merge-holds.mjs:245: matchesHoldPath is posix.matchesGlob on undotted paths
    • /home/runner/work/claude-code-plugins/claude-code-plugins/.base/.github/workflows/pr-require-checks.yml:4767: generated hold_patterns block

    To answer, post a comment that starts with this line:

    **Decision recorded.** <answer>
    

    Automated refinement by the lanes App. Answer with a comment whose first line is **Decision recorded.**.

  8. kyle-sexton commented on Oct 29, 2026

    @kyle-sexton
    MemberAuthor

    Two more gaps from the delta security review of #10743's 88bf5c9 (no HIGH/CRITICAL; neither is a loosening by that commit):

    1. The scripts a gated workflow runs are not gated with it. feat(pr-pipeline): bind workflow-edit reviews to a run from main #10743 gates workflows holding actions: write, because an edit inside existing scopes trips no patch hold. The dispatch logic is mostly outside the floor: .github/scripts/intake-reenter/reenter.mjs (its WORKFLOW and ref constants pick what the actions: write token dispatches), maintenance-reconcile-lost-runs/reconcile.mjs, post-merge-demotion-alert/{report,failing-checks}.mjs, and pr-unblock/{dispatch,fix-ci-dispatch,fix-docs-dispatch,dependency-dispatch}.mjs. A PR that re-points one of them leaves the workflow file unchanged and clears on a model verdict. Fix: have scripts/sync-hold-floor.py derive the scripts each gated workflow runs (as test_the_floor_holds_what_its_actions_load derives action closures) and hold them gate.
    2. Escaped YAML forms of actions: write / id-token: write miss the text regex. actions: "\x77rite", "\x61ctions": write, actions: !!str write, and block scalars (actions: >- then write). Landing such a grant already holds through the YAML-parsed privilege-gain check, so this matters only for later edits. Fix: in gated_workflow, when PyYAML is available, decide from the parsed permissions (_workflow_privileges), keeping the regex as the fallback.
  9. kyle-sexton commented on Oct 29, 2026

    @kyle-sexton
    MemberAuthor

    Gap found while building #10966: lib/publish-gate.mjs is on no hold-floor row, yet .github/scripts/pr-review-main/write-verdict.mjs (a person-held gate path) imports it, so a PR can change how main-run verdicts are written with no hold. Not fixed in #10966: adding a person-held row adds an owner stop, which needs the owner's decision first. Options: hold it gate (one more owner stop on edits to it), or fold it into a lane-armed class once the #10966 attestation lands.

  10. changed the title [-]fix(source-control): close five pre-existing secret and hold-path detection gaps[/-] [+]fix(source-control): close five pre-existing secret and hold-path detection gaps (hardening deferred)[/+] on Oct 30, 2026
  11. kyle-sexton commented on Oct 30, 2026

    @kyle-sexton
    MemberAuthor

    Decision recorded. Hardening deferred, closed as not planned under owner standing direction §10: "Phase: maximum throughput until open issues and open PRs both reach 0. Hardening (re-adding holds, pins, reviews) is deferred to after that, as configuration edits."

    Each of the five gaps is reachable only by our own actors. The secret-path ones depend on secrets: inherit, which passes secrets only to workflows in the same organization or enterprise (https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows), so no outside actor can reach a token through them. §10 counts only a token or secret leak, account compromise or billing as a §3 boundary. Secret-leak scanning (gitleaks) stays on, as do both review lanes and ci-status (owner Q15). GitHub Support agreed with this classification.

    Reopen trigger: open issues = 0 and open PRs = 0. The hardening pass finds this with is:issue is:closed reason:"not planned" in:title "hardening deferred".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority: mediumReal value, no hard deadline; normal backlog flow.status: confirmedA maintainer confirmed the item is real and in scope; ready to be worked.status: needs-decisionAwaiting a human or maintainer judgment call.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions