fix(ci): sandbox the DoR build agents from untrusted issue input and tokens; tidy CI permissions and deps (SEC-2026-09 H-05, L-19, I-01) - #1194
Open
WimvandenHeijkant wants to merge 6 commits into
Conversation
…-09 H-05) The build agent has a shell, and its spec used to be the whole issue thread of a public repository. dor_trusted_spec.sh keeps the issue body and comments by the requestor of record, org members (checked with a Members:read token) and the pipeline's own bot identities, drops everything else, and records how much was left out. A membership lookup that errors counts as untrusted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…guard its output (SEC-2026-09 H-05) - The claude CLI starts with every GitHub credential removed from its environment; CLAUDE_CODE_OAUTH_TOKEN stays because the CLI needs it. - The workflows stage GH_TOKEN/BOARD_TOKEN as files the flow loads and deletes, so they are not in the flow's own process environment either. - origin is no longer rewritten to a token-bearing URL; pushes pass the app token per command only. - After each agent run the checkout's git config and hooks are restored, and the flow's git ignores global/system config and hooks. - A branch that changes .github/ is refused before any push (Exceptions); Dockerfile, compose and package.json dependency changes are flagged on the PR. - The value-gate policy reads the repro contract through the trusted filter, and the approval notice only links a bot-authored certified spec. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d site deploy workflow (SEC-2026-09 L-19) pr.yml and pr-integration.yml now default GITHUB_TOKEN to contents: read; only the path-filter jobs add pull-requests: read. The new DoR self-tests run in the ci-scripts job. deploy-site.yml was never enabled, used tag-pinned actions and referenced a deployment secret; the site is self-hosted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-copy-streams (SEC-2026-09 I-01) - overrides uuid ^11.1.1 in app/api and app/ui (exceljs 4.4.0 pulled uuid 8); exporter tests and a real xlsx write pass with it. npm audit --omit=dev: 0. - remove the vestigial patch-package postinstall and devDependency (there is no patches/ directory) and the unused pg-copy-streams dependency, with the SBOM page, About page and ingest engine comment that still named it. - changelog fragment for this branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A long thread (a real one: 30 comments) exceeds the kernel's per-argument limit when handed to jq --argjson, which would have failed the build step. The filter now works on temp files; a ~300 KB comment is covered by a test that fails against the argv version. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… CI runners The integration suite starts every crawler's tests at once, and each mock server is a fresh pwsh job process. The OData mock gave that job 4 s and failed with an empty output on a loaded runner (three runs in a row on this PR, and on another branch the same day). The OData mock now uses the shared Start-MockServerJob helper like the SCIM and midPoint mocks, and the helper waits up to 30 s; a server that starts still returns as soon as it reports. A Pester test with a 5 s start-up fails against the old window. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Summary
Security fixes from the SEC-2026-09 review for the CI and "Definition of Ready" (DoR) build automation, plus dependency tidying:
.github/is stopped before push.pr.ymlandpr-integration.ymlnow defaultGITHUB_TOKENtocontents: read. The unuseddeploy-site.ymlis removed.uuidis overridden to a patched version forexceljs, and the unusedpatch-packageandpg-copy-streamsdependencies are removed.Findings addressed
.github/scripts/dor_trusted_spec.shbuildsspec.jsonfrom trusted authors only: the requestor of record, org members (checked with amembers:readapp token, the same grantdor-authorizeuses), and the pipeline bot identities (RESTtype == "Bot"plus an exact[bot]login). All other comments are dropped and counted in atrustblock. If the author is untrusted (a vouched external request), the issue title and body are withheld too. Membership lookup errors fail closed. The value-gatepolicyjob reads the repro contract through the same filter, so a look-alike contract from an outside account cannot skip the human gate. The approval notice only links a bot-authored certified spec. AcceptanceFEEDBACKwas already gated on the commenter's org membership (actor: github.actor); I verified that and left it unchanged.run_claudestarts the CLI throughagent_exec, which stripsGH_*,GITHUB_TOKEN,BOARD_TOKEN,ACTIONS_*and*_TOKEN/_SECRET/_PASSWORD/_PRIVATE_KEY/_API_KEY/_PATfrom its environment.CLAUDE_CODE_OAUTH_TOKENis kept because the CLI needs it. The build and feedback workflows now stageGH_TOKEN/BOARD_TOKENas0600files that the flow loads and deletes before any agent runs, so the tokens are also absent from the flow process's own environment (which a child could read from/proc).use_bot_remoteno longer writes the app token into theoriginURL: origin stays anonymous (the repo is public) andpush_as_apppasses the token per command. After every agent run the flow restores.git/configfrom an in-memory snapshot and clears.git/hooks. Its own git ignores global/system config and hooks (GIT_CONFIG_GLOBAL=/dev/null,GIT_CONFIG_NOSYSTEM,core.hooksPath/core.fsmonitoroverridden viaGIT_CONFIG_COUNT).actions/checkoutalready usedpersist-credentials: false, anddrop_checkout_credentialsis kept.guard_protected_pathsruns before every push (initial build, CI auto-fix, feedback adjustment, usage-limit pause). It re-fetchesmainand bails to Exceptions whenorigin/main...HEADtouches.github/. That includes commits the agent made itself, which the old restore-from-HEAD could not undo.push_as_apprefuses the same case as a backstop, and fails closed when there is nomainto compare with. Dockerfile, compose andpackage.jsondependency/override/install-script changes are flagged but not blocked: in the PR body when the PR opens, and as a PR comment if the set changes later.permissions: contents: readinpr.ymlandpr-integration.yml. Thefilterjobs addpull-requests: readbecausedorny/paths-filterlists PR files through the API. I read every job in both files: none posts comments, check annotations or SARIF, and artifacts plus the buildxghacache use the runner's runtime token.deploy-site.ymlis deleted. Nothing referenced it exceptsite/README.md, which is updated."uuid": "^11.1.1"added tooverridesinapp/apiandapp/ui; both lockfiles now resolveuuid@11.1.1.patch-packagepostinstall and devDependency removed fromapp/api(there is nopatches/dir).pg-copy-streamsremoved: nothing imports it, andengine.jsalready documented why COPY is not used. The SBOM page, About page and an engine comment still named it and are updated.npm audit --omit=dev: 0 vulnerabilities in both packages.Not addressed / deferred
bugfixes/sec-deployment-hardening).dor-agent.yml/dor-bug-agent.yml) still reads the whole thread. That is deliberate: the interview flow has to hear external requesters, and that agent is already sandboxed (Read/Grep/Glob plusEdit(.dor/out/**), no shell, no GitHub token). Its output reaches the build agent only as the certified comment, which a human value gate reviews for features. For autonomous bugs, the build-side guards above apply.dockergroup and has open egress. A deliberately malicious process can still act with that user's rights: tamper with~/.local/bin, read runner files, or start containers. This PR removes every passive route to the tokens and blocks the unauthenticated trigger. The actual boundary needs work on the sidekick itself (see Manual follow-ups).docs/process/dor-sidekick-setup.mdnow states this plainly.Compatibility
dor_build_lib.shstill acceptsGH_TOKEN/BOARD_TOKENfrom the environment whenDOR_CRED_DIRis unset (direct runs, tests). Pushes still authenticate as the app, per command, aspush_as_appalready did. Fetches were already anonymous-capable, since the repo is public.push_as_app.pause_and_exitpreviously pushed via the token-bearingorigin. The anonymous origin would have broken that, so it now usespush_as_app, same identity.spec.jsonwithholds the title and body and the build works from the certified spec comment. Comments and the certified spec from the requestor of record (the voucher) and other members are unaffected.uuid11 keeps thev4()exportexceljsuses (CJS and ESM). The two removed dependencies were not loaded at runtime.Tests
test/ci-scripts/test-dor-trusted-spec.sh(26 assertions), wired into theci-scriptsPR job. Discriminating inputs: a User account named like the bot without[bot], a foreign Bot account, a member whose check must use the Members:read token (the stub refuses any other token), a membership API error, a repeat commenter (looked up once), an outsider-authored issue, and an unreadable comment list (the step must fail).test/ci-scripts/test-dor-agent-sandbox.sh(41 assertions), wired intoci-scripts, using real git on local repos with stubclaude/gh. It checks that:CLAUDE_CODE_OAUTH_TOKEN;url.insteadOfand hook are undone, and hooks never run under the flow's git;.github/change is refused by the guard and bypush_as_app, including after movingorigin/mainonto it, and fails closed withoutmain;descriptionedit is not, flags never block a push, and an unchanged flag set is not re-posted.GH_TOKEN/BOARD_TOKEN, origin holds the token, a planted hook runs, and a.github/commit is pushed.test/ci-scripts/test-dor-push-lease.shupdated: the fixture has amain, and it now asserts thatuse_bot_remoteleaves origin tokenless (19 passed).-S warning: new scripts clean, touched scripts unchanged (7 pre-existing findings before and after). actionlint on the four edited workflows: only the pre-existing customdor-buildrunner-label notice.app/api:npx vitest run233 files / 3025 passed;npm run lint0 errors (5 pre-existing warnings). Exporter testexcelWorkbook.test.js17 passed. A realexceljsxlsx write succeeds withuuid@11.1.1.app/ui:npx vitest run199 files / 1890 passed;npm run lint0 errors (4 pre-existing warnings). Exporter tests (exportToExcel,exportAccessPackagesToExcel,MatrixView.mount) 52 passed.Start-MockServerJobhelper, which waits up to 30 s and still returns as soon as the server reports. Newtest/unit/MockServerJob.Tests.ps1(3 tests) includes a 5 s start-up case that fails against the old window.Manual follow-ups
mainruleset bypass list. Givebump-version.yml(and any other workflow that pushesmain) its own narrowly-scoped identity instead of the app the DoR build agent's flow uses.dockergroup membership, with an egress allow-list (Anthropic API, npm registry, GitHub). Never give that environment the runner's credentials or home directory.CLAUDE_CODE_OAUTH_TOKEN, since it was reachable by the agent until now and still is by design. Delete the now-unusedAZURE_STATIC_WEB_APPS_API_TOKENrepository secret, and the Static Web App deployment token in Azure if one exists.🤖 Generated with Claude Code