Skip to content

Comments

Fix: reject malformed/truncated ${{ secrets. expressions in processExpressions#18171

Merged
pelikhan merged 2 commits intomainfrom
copilot/fix-fuzz-job-runtime-import
Feb 24, 2026
Merged

Fix: reject malformed/truncated ${{ secrets. expressions in processExpressions#18171
pelikhan merged 2 commits intomainfrom
copilot/fix-fuzz-job-runtime-import

Conversation

Copy link
Contributor

Copilot AI commented Feb 24, 2026

processExpressions only matched well-formed ${{ ... }} blocks via regex, so a truncated expression like ${{ secrets.TOKEN }T (missing }}) produced zero matches and returned the original content as safe. The fuzz harness caught this because its invariant check (/\$\{\{[^}]*secrets\./) correctly flags such inputs as security violations.

Changes

  • actions/setup/js/runtime_import.cjs — After computing matches, add a guard using /\$\{\{[^}]*secrets\./. If that pattern is present in content but none of the well-formed captures (m[1]) contain secrets., throw ERR_VALIDATION immediately. Well-formed ${{ secrets.X }} expressions remain handled by the existing isSafeExpression path.

  • pkg/parser/testdata/fuzz/FuzzRuntimeImportProcessExpressions/9661e56aa0ec2781 — Adds the minimized corpus entry (Secret: ${{ secrets.TOKEN }T) that reproduces the fuzz failure, pinning the invariant against regression.

// Before: returns content unchanged — ${{ secrets.TOKEN }T slips through
const matches = [...content.matchAll(/\$\{\{([\s\S]*?)\}\}/g)];
if (matches.length === 0) return content;  // ← bug: malformed secrets not caught

// After: partial secrets expression rejected before early return
const partialSecretsRegex = /\$\{\{[^}]*secrets\./;
if (partialSecretsRegex.test(content)) {
  const wellFormedSecretsMatched = matches.some(m => /secrets\./.test(m[1]));
  if (!wellFormedSecretsMatched) throw new Error(`${ERR_VALIDATION}: ...`);
}

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw -test.run=^Test -test.short=trueHEAD git rev-�� --show-toplevel resolved$ /usr/bin/unpigz -json GO111MODULE x_amd64/vet /usr/bin/unpigz (http block)
  • https://api.github.com/repos/actions/ai-inference/git/ref/tags/v1
    • Triggering command: /usr/bin/gh gh api /repos/actions/ai-inference/git/ref/tags/v1 --jq .object.sha -unreachable=false l 3978800/b389/vet.cfg d7ab9ec12dbe903egit GO111MODULE 64/bin/go /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet -uns�� -unreachable=false /tmp/go-build1583978800/b263/vet.cfg ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet -json GO111MODULE 64/bin/go ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v3
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v3 --jq .object.sha 374/001/stability-test.md 81c88388fc5569beec7cf4cb:go.mod ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v4 --jq .object.sha g_.a GO111MODULE /opt/hostedtoolcache/go/1.25.0/x-buildmode=exe GOINSECURE GOMOD GOMODCACHE /opt/hostedtoolcache/go/1.25.0/x-extld=gcc -uns�� sAOsG8WsF /tmp/go-build1583978800/b057/vet.cfg 0/x64/bin/node GOSUMDB GOWORK 64/bin/go /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v4 --jq .object.sha 76 GO111MODULE /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet -uns�� runs/20260224-153838-27546/test-3934205799/.github/workflows /tmp/go-build1583978800/b104/vet.cfg 0/x64/bin/node l GOWORK 64/bin/go /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v4 --jq .object.sha --show-toplevel go 6786/001/go/1.25.0/x64/bin/go -json GO111MODULE x_amd64/vet git conf�� user.name Test User /usr/bin/git ub/workflows GO111MODULE x_amd64/vet git (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v5
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha 932779234/.github/workflows .cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet env -json .cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha se 3978800/b110/vet.cfg /usr/bin/git -errorsas -ifaceassert -nilfunc git -C /tmp/compile-instructions-test-3629014071/.github/workflows rev-parse /usr/bin/git --check **/*.cjs 64/bin/go git (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha --show-toplevel x_amd64/vet 3978800/b433/vet.cfg b/workflows GO111MODULE x_amd64/vet git rev-�� --show-toplevel x_amd64/vet /tmp/go-build1583978800/b431/workflow.test -json GO111MODULE x_amd64/vet /tmp/go-build1583978800/b431/workflow.test (http block)
  • https://api.github.com/repos/actions/github-script/git/ref/tags/v8
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha npx prettier --cGOSUMDB GOPROXY 64/bin/go GOSUMDB GOWORK 64/bin/go sh -c "prettier" --check 'scripts/**/*GOINSECURE node 64/bin/go tierignore ../../../pkg/wor-atomic 64/bin/go go (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha k/gh-aw/gh-aw/pk-errorsas k/gh-aw/gh-aw/pk-ifaceassert 64/bin/go **/*.json --ignore-path ../../../.pretti-unreachable=false /opt/hostedtoolc/tmp/go-build1583978800/b265/vet.cfg -o /tmp/go-build1481836524/b400/_pkGOINSECURE -trimpath 64/bin/go -p main -lang=go1.25 go (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha prettier --check 64/bin/go --ignore-path .prettierignore 64/bin/go go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go (http block)
  • https://api.github.com/repos/actions/setup-go/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-go/git/ref/tags/v4 --jq .object.sha -json GO111MODULE /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet -uns�� runs/20260224-153838-27546/test-2152807819/.github/workflows /tmp/go-build1583978800/b067/vet.cfg 3978800/b336/vet.cfg GOSUMDB GOWORK 64/bin/go /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/actions/setup-node/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v4 --jq .object.sha tutil.test GO111MODULE ortcfg.link GOINSECURE GOMOD GOMODCACHE xLPGO5v5ZsAOsG8WsF/788KaSlanoh1QIKEP8dB/iAJz33n4qBSgoBRlrikz -uns�� -unreachable=false /tmp/go-build1583978800/b051/vet.cfg g_.a GOSUMDB GOWORK 64/bin/go /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/1/artifacts
    • Triggering command: /usr/bin/gh gh run download 1 --dir test-logs/run-1 GO111MODULE x_amd64/vet GOINSECURE GOMOD GOMODCACHE x_amd64/vet env -json .cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/12345/artifacts
    • Triggering command: /usr/bin/gh gh run download 12345 --dir test-logs/run-12345 GO111MODULE 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet env ub/workflows .cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/12346/artifacts
    • Triggering command: /usr/bin/gh gh run download 12346 --dir test-logs/run-12346 GO111MODULE 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet env ub/workflows .cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linutest@example.com (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/2/artifacts
    • Triggering command: /usr/bin/gh gh run download 2 --dir test-logs/run-2 GO111MODULE x_amd64/compile GOINSECURE GOMOD GOMODCACHE x_amd64/compile env -json .cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/3/artifacts
    • Triggering command: /usr/bin/gh gh run download 3 --dir test-logs/run-3 GO111MODULE x_amd64/link GOINSECURE GOMOD GOMODCACHE x_amd64/link env -json .cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE HC/wPHmRHH07drGotDxh6_4/9rUbv3kNVNgnGPLEQds7 (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/4/artifacts
    • Triggering command: /usr/bin/gh gh run download 4 --dir test-logs/run-4 GO111MODULE x_amd64/vet GOINSECURE GOMOD GOMODCACHE x_amd64/vet env -json .cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/5/artifacts
    • Triggering command: /usr/bin/gh gh run download 5 --dir test-logs/run-5 GO111MODULE x_amd64/vet GOINSECURE GOMOD GOMODCACHE x_amd64/vet env -json .cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/github/gh-aw/actions/workflows
    • Triggering command: /usr/bin/gh gh workflow list --json name,state,path prettier --check 64/bin/go **/*.ts **/*.json --ignore-path go env -json GOMOD 64/bin/go tierignore format:cjs 64/bin/go go (http block)
    • Triggering command: /usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 100 (http block)
    • Triggering command: /usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 6 GOMOD GOMODCACHE x_amd64/vet env -json GO111MODULE 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linutest@example.com (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.0.0 --jq .object.sha -json rty 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet env -json .cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/nonexistent/action/git/ref/tags/v999.999.999
    • Triggering command: /usr/bin/gh gh api /repos/nonexistent/action/git/ref/tags/v999.999.999 --jq .object.sha y-test.md GO111MODULE 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet env 4165525112/.github/workflows .cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/nonexistent/repo/actions/runs/12345
    • Triggering command: /usr/bin/gh gh run view 12345 --repo nonexistent/repo --json status,conclusion GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet env -json .cfg 64/pkg/tool/linux_amd64/vet GOINSECURE GOMOD GOMODCACHE 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/owner/repo/actions/workflows
    • Triggering command: /usr/bin/gh gh workflow list --json name,state,path --repo owner/repo 64/bin/go **/*.ts **/*.json --ignore-path go list�� -mod=readonly -f 64/bin/go -- unsafe 64/bin/go go (http block)
    • Triggering command: /usr/bin/gh gh workflow list --json name,state,path --repo owner/repo 64/bin/go **/*.ts **/*.json --ignore-path git stat�� --porcelain sh 64/bin/go npx prettier --w/opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/owner/repo/contents/file.md
    • Triggering command: /tmp/go-build1583978800/b381/cli.test /tmp/go-build1583978800/b381/cli.test -test.testlogfile=/tmp/go-build1583978800/b381/testlog.txt -test.paniconexit0 -test.v=true -test.parallel=4 -test.timeout=10m0s -test.run=^Test -test.short=true GOINSECURE GOMOD GOMODCACHE npx pret�� --check scripts/**/*.js 64/bin/go .prettierignore GOWORK 64/bin/go go (http block)
  • https://api.github.com/repos/test-owner/test-repo/actions/secrets
    • Triggering command: /usr/bin/gh gh api /repos/test-owner/test-repo/actions/secrets --jq .secrets[].name prettier --check 64/bin/go **/*.ts **/*.json --ignore-path go env GOPATH node 64/bin/go -d format:cjs 64/bin/go go (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>[CI Failure Doctor] Runtime import fuzz accepts truncated secrets expression in run 22356701883</issue_title>
<issue_description>### CI Failure Investigation - Run github/gh-aw#37716

Summary

The fuzz job now fails because processExpressions treats Secret: $\{\{ secrets.TOKEN }T as safe: the regex only matches well-formed $\{\{ ... }} blocks, so truncated expressions that still contain $\{\{ secrets. slip through and the harness complains after the function already returned success.

Failure Details

Root Cause Analysis

processExpressions stops at const expressionRegex = /\$\{\{([\s\S]*?)\}\}/g, so inputs that do not finish the }} (for example Secret: $\{\{ secrets.TOKEN }T) never register as expressions. The loop never evaluates them or rejects them, so the function returns the original string even though the payload contains secrets.. runtime_import_fuzz_test.go asserts that any secrets. expression should fail, so the fuzz target crashes once the corpus entry is minimized.

Failed Jobs and Errors

  • fuzz (FuzzRuntimeImportProcessExpressions) – runtime_import_fuzz_test.go:260: Content with $\{\{ secrets. expression was processed successfully, and result still contains Secret: $\{\{ secrets.TOKEN }T before the harness bails out.
Investigation Findings
  • Replicate locally with go test -run=FuzzRuntimeImportProcessExpressions/9661e56aa0ec2781 ./pkg/parser; the failing corpus lives at testdata/fuzz/FuzzRuntimeImportProcessExpressions/9661e56aa0ec2781.
  • The logs show runtime_import_fuzz_test.go:270 warning that the result still contains $\{\{ secrets.TOKEN }T, which proves processExpressions never consumed the expression.
  • Because the regex requires }}, truncated closing braces bypass the evaluator even though the security invariant is that secrets. expressions must never succeed.

Recommended Actions

  • Detect $\{\{ secrets. sequences even when the closing braces are malformed and treat them as unsafe before returning success from processExpressions.
  • Add a regression that replays testdata/fuzz/FuzzRuntimeImportProcessExpressions/9661e56aa0ec2781 or a similar truncated expression to ensure the harness fails instead of reporting success.
  • Log this pattern so future additions to processExpressions also consider partial or malformed markers as security violations.

Prevention Strategies

  • Keep the runtime import fuzz targets running in CI to catch new variations of malformed secrets expressions.
  • Consider sanitizing or normalizing the $\{\{/}} markers before the regex match so invisible bytes or missing braces can’t bypass detection.

AI Team Self-Improvement

When inspecting runtime import failures, always check whether $\{\{ secrets. survives the process even if the regex never matches, and add fuzz corpus entries for truncated markers so new fixes keep the invariant alive.

Historical Context

#18165 described a similar bypass where bytes inserted between $ and \{\{ prevented the regex from matching. This run shows the same weakness in the regex when the closing }} is corrupted or missing, so treat both signatures as part of the same regression.

🩺 Diagnosis provided by CI Failure Doctor

To install this workflow, run gh aw add githubnext/agentics/workflows/ci-doctor.md@ea350161ad5dcc9624cf510f134c6a9e39a6f94d. View source at https://github.com/githubnext/agentics/tree/ea350161ad5dcc9624cf510f134c6a9e39a6f94d/workflows/ci-doctor.md.

  • expires on Feb 25, 2026, 3:18 PM UTC

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Add a guard in processExpressions that rejects content containing a partial
${{ secrets. pattern (e.g. ${{ secrets.TOKEN }T with missing }}) that bypasses
the well-formed expression regex. Also add fuzz corpus entry 9661e56aa0ec2781
to prevent regression.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix truncated secrets expression handling in fuzz job Fix: reject malformed/truncated ${{ secrets. expressions in processExpressions Feb 24, 2026
@pelikhan pelikhan marked this pull request as ready for review February 24, 2026 15:48
Copilot AI review requested due to automatic review settings February 24, 2026 15:48
@pelikhan pelikhan merged commit e7963ef into main Feb 24, 2026
83 checks passed
@pelikhan pelikhan deleted the copilot/fix-fuzz-job-runtime-import branch February 24, 2026 15:48
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a security vulnerability where malformed or truncated ${{ secrets. expressions bypass validation in processExpressions. The fuzzer discovered that expressions like ${{ secrets.TOKEN }T (missing closing }}) were not caught because the regex only matched well-formed ${{ ... }} blocks.

Changes:

  • Added early validation check to detect partial ${{ secrets. patterns before they bypass the main expression regex
  • Added fuzz corpus entry to prevent regression of this specific vulnerability

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
actions/setup/js/runtime_import.cjs Adds security guard to detect and reject malformed/truncated secrets expressions that bypass the well-formed expression regex
pkg/parser/testdata/fuzz/FuzzRuntimeImportProcessExpressions/9661e56aa0ec2781 Adds minimized fuzz corpus entry reproducing the vulnerability to prevent future regressions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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.

[CI Failure Doctor] Runtime import fuzz accepts truncated secrets expression in run 22356701883

2 participants