Skip to content

Close detector bypasses: bracket env, unqualified getenv, custom-shell PR head#20

Merged
Conalh merged 1 commit into
mainfrom
close-detector-bypasses
May 22, 2026
Merged

Close detector bypasses: bracket env, unqualified getenv, custom-shell PR head#20
Conalh merged 1 commit into
mainfrom
close-detector-bypasses

Conversation

@Conalh
Copy link
Copy Markdown
Owner

@Conalh Conalh commented May 22, 2026

Summary

Closes three real detection bypasses surfaced by a follow-up inspection of CapabilityEcho's regex-based detectors. All three are small, mechanical broadenings — same shape as agent-gov-core's "match what the agent will actually write" philosophy. No schema changes; one kind reused with broader semantic.

  • JS bracket-notation process.envjs-capability.ts: addSecretVariable and referencesEnvSecret only matched dot notation. process.env['API_TOKEN'] / process.env[\"API_TOKEN\"] slipped both, so an inline exfil expression OR a stored secret variable referenced later was missed. Now both notations match.
  • Python unqualified getenv / environ.getpy-capability.ts: both helpers required the os. prefix, so from os import getenv; k = getenv('API_TOKEN') (and the environ variant) bypassed secret-variable tracking. os. is now optional in both detection points. The secret-name suffix pattern (TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH) keeps false positives bounded.
  • PR-head reference under pull_request_target via custom shellworkflow-permissions.ts: isPullRequestHeadCheckoutLine only matched structured ref:/repository: keys. A workflow could trivially bypass by using a run: step:
    run: |
      git clone https://github.com/\${{ github.event.pull_request.head.repo.full_name }}
      git checkout \${{ github.event.pull_request.head.sha }}
    Now any reference to github.event.pull_request.head.{sha,ref,repo.full_name} inside a pull_request_target workflow is flagged. Function renamed referencesPullRequestHead; subject/message/recommendation reworded from "checkout" to "reference" to reflect the broadened semantic. kind (workflow_pr_head_checkout_on_target) preserved for downstream policy stability.

Why batched

All three are "broaden a regex to catch an obvious bypass" — same theme, low surface, no API changes. Total source delta is ~30 lines; tests add 6 cases.

Deferred (acknowledged from the same inspection)

  • Multi-line statement context (yaml.load(...) across lines, multi-line YAML env secret values) — needs file-content lookback infrastructure; separate PR with shared helpers.
  • Directory-mode git-failure hardening — silent bypass on modified files when git diff --no-index returns empty stdout. Real but narrow (git is always present on GHA runners); defensive fix in its own PR.
  • Stream-based git diff — speculative for now; we haven't seen 20MB+ diffs in practice.

Declined: CLI parser refactor to mri/yargs-parser — adds a runtime dep to a 6-flag CLI on a security-adjacent tool.

Stacking note

This PR is independent of #19 ("Tighten CLI parity, monorepo annotations, and workflow noise"). The two touch different functions in workflow-permissions.ts and different parts of report.ts; either order of merge is fine. Whichever lands second will need a one-line bundle rebuild rebase, no source conflicts expected.

Test plan

  • npm test — 70 passing (64 baseline + 6 new):
    • JS: bracket-notation inline exfil flagged; bracket-notation secret variable tracked across lines
    • Python: unqualified getenv inline exfil flagged; getenv and environ.get tracked across lines (with file context)
    • Workflow: custom-shell git clone/git checkout referencing head.{sha,repo.full_name} both flag under pull_request_target
  • npm run build — clean tsc + ncc bundle
  • Reviewer: confirm the broader PR-head reference detector doesn't fire on benign metadata uses you actually have in real workflows (e.g. \${{ github.event.pull_request.head.repo.owner.login }} is not matched — only sha, ref, repo.full_name)

🤖 Generated with Claude Code

Conalh added a commit that referenced this pull request May 22, 2026
…l and merge ref

Extends #20's bypass closures with the next wave of evasions surfaced
by external inspection.

- JS destructuring (`const { API_TOKEN } = process.env`) and renamed
  destructuring (`const { API_TOKEN: t } = process.env`) now track the
  secret variable. addSecretVariable handles both direct and
  destructured forms.
- Python aliased env imports: `from os import getenv as g` /
  `environ as e` (and the unaliased `from os import getenv` form
  already supported in #20) build a per-file alias map, then the
  secret-variable regex is generated dynamically from the alias union.
  Closes `token = g("API_TOKEN")` bypass.
- Workflow referencesPullRequestHead now covers
  github.event.pull_request.head.repo.clone_url and standalone
  refs/pull/<n>/merge references — the two custom-shell patterns
  agents use when actions/checkout would attract review attention.
- README: new "Detection limits" section documenting same-line URL
  requirement, no cross-file taint, no Python dep manifests yet, with
  a pointer to test/fixtures/bypasses/.
- test/fixtures/bypasses/ established as the bypass-corpus pattern,
  with one fixture per closure and a CLI integration test per fixture.

Adds 10 tests (80 total on this branch).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…l PR head)

- JS: addSecretVariable and referencesEnvSecret now match bracket
  notation (process.env['KEY'] / ["KEY"]) alongside dot notation. An
  inline `Authorization: Bearer ${process.env['API_TOKEN']}` or a stored
  `const x = process.env["API_TOKEN"]` referenced later now flag the
  exfil pattern.
- Python: addSecretVariable and referencesPyEnvSecret make the `os.`
  prefix optional, so `from os import getenv; k = getenv("API_TOKEN")`
  and `from os import environ; k = environ.get("API_KEY")` are tracked
  as secret variables and trigger exfil on later external requests.
- Workflows: detectPullRequestHeadCheckoutOnTarget no longer requires a
  structured `ref:`/`repository:` key — any reference to
  github.event.pull_request.head.{sha,ref,repo.full_name} under a
  pull_request_target workflow is flagged. Closes a bypass where a
  custom `run:` step with `git checkout ${{ ... head.sha }}` skipped
  detection. Subject/message/recommendation updated from "checkout" to
  "reference" to reflect the broadened semantic; kind preserved for
  back-compat.

Adds 6 tests (70 total, all passing on this branch's baseline).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Conalh Conalh force-pushed the close-detector-bypasses branch from afa1262 to da04278 Compare May 22, 2026 23:56
@Conalh Conalh merged commit b29d5ba into main May 22, 2026
3 of 4 checks passed
@Conalh Conalh deleted the close-detector-bypasses branch May 22, 2026 23:56
Conalh added a commit that referenced this pull request May 22, 2026
…l and merge ref

Extends #20's bypass closures with the next wave of evasions surfaced
by external inspection.

- JS destructuring (`const { API_TOKEN } = process.env`) and renamed
  destructuring (`const { API_TOKEN: t } = process.env`) now track the
  secret variable. addSecretVariable handles both direct and
  destructured forms.
- Python aliased env imports: `from os import getenv as g` /
  `environ as e` (and the unaliased `from os import getenv` form
  already supported in #20) build a per-file alias map, then the
  secret-variable regex is generated dynamically from the alias union.
  Closes `token = g("API_TOKEN")` bypass.
- Workflow referencesPullRequestHead now covers
  github.event.pull_request.head.repo.clone_url and standalone
  refs/pull/<n>/merge references — the two custom-shell patterns
  agents use when actions/checkout would attract review attention.
- README: new "Detection limits" section documenting same-line URL
  requirement, no cross-file taint, no Python dep manifests yet, with
  a pointer to test/fixtures/bypasses/.
- test/fixtures/bypasses/ established as the bypass-corpus pattern,
  with one fixture per closure and a CLI integration test per fixture.

Adds 10 tests (80 total on this branch).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Conalh added a commit that referenced this pull request May 22, 2026
…l and merge ref

Extends #20's bypass closures with the next wave of evasions surfaced
by external inspection.

- JS destructuring (`const { API_TOKEN } = process.env`) and renamed
  destructuring (`const { API_TOKEN: t } = process.env`) now track the
  secret variable. addSecretVariable handles both direct and
  destructured forms.
- Python aliased env imports: `from os import getenv as g` /
  `environ as e` (and the unaliased `from os import getenv` form
  already supported in #20) build a per-file alias map, then the
  secret-variable regex is generated dynamically from the alias union.
  Closes `token = g("API_TOKEN")` bypass.
- Workflow referencesPullRequestHead now covers
  github.event.pull_request.head.repo.clone_url and standalone
  refs/pull/<n>/merge references — the two custom-shell patterns
  agents use when actions/checkout would attract review attention.
- README: new "Detection limits" section documenting same-line URL
  requirement, no cross-file taint, no Python dep manifests yet, with
  a pointer to test/fixtures/bypasses/.
- test/fixtures/bypasses/ established as the bypass-corpus pattern,
  with one fixture per closure and a CLI integration test per fixture.

Adds 10 tests (80 total on this branch).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant