From 5fa8bf5d5e53411fb84ddfbcbadc4852193adabc Mon Sep 17 00:00:00 2001 From: Dustin Date: Mon, 13 Apr 2026 00:09:21 -0700 Subject: [PATCH] fix: GitHub Actions shell injection in _build.yml (#247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #247. Replaced direct `${{ ... }}` interpolation in `run:` steps with `env:` blocks per GitHub's hardening guide. Every workflow input (inputs.version) and matrix value (matrix.cc, matrix.cxx, matrix.goos, matrix.goarch, matrix.arch) now flows into the shell through a named environment variable rather than being textually spliced into the script. Pattern applied uniformly across the 6 flagged steps (lines 55, 68, 111, 123, 162, 176 pre-change) plus the archive steps that used ${{ matrix.* }} in tar filenames: env: VERSION: ${{ inputs.version }} CC: ${{ matrix.cc }} CXX: ${{ matrix.cxx }} run: | if [ -n "$VERSION" ]; then scripts/build.sh --version "$VERSION" "CC=$CC" "CXX=$CXX" else scripts/build.sh "CC=$CC" "CXX=$CXX" fi This is safe against injection because: 1. The `env:` block treats values as opaque strings during YAML parsing; GitHub does not parse shell metacharacters in env values 2. $VAR expansion in the script uses normal shell semantics, not substitution — attacker input in $VERSION becomes a literal argv[] entry, not interpreted shell code 3. The if/else form avoids word-splitting pitfalls of conditional arg construction Verification: - code-audit scan on _build.yml: 6 shell-injection findings → 0 - yaml.safe_load(_build.yml) parses cleanly - Behavior preservation: same build.sh invocations, same artifact names, same conditional version handling. The only functional difference is the version-handling branch is now an if/else instead of a YAML ternary, which produces byte-identical arguments to build.sh Reference: https://securitylab.github.com/research/github-actions-untrusted-input/ --- .github/workflows/_build.yml | 76 +++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index ea9fabe9..cdad2af3 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -52,20 +52,41 @@ jobs: node-version: "22" - name: Build standard binary - run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=${{ matrix.cc }} CXX=${{ matrix.cxx }} + env: + VERSION: ${{ inputs.version }} + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} + run: | + if [ -n "$VERSION" ]; then + scripts/build.sh --version "$VERSION" "CC=$CC" "CXX=$CXX" + else + scripts/build.sh "CC=$CC" "CXX=$CXX" + fi - name: Ad-hoc sign macOS binary if: startsWith(matrix.os, 'macos') run: codesign --sign - --force build/c/codebase-memory-mcp - name: Archive standard binary + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} run: | cp LICENSE install.sh build/c/ - tar -czf codebase-memory-mcp-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \ + tar -czf "codebase-memory-mcp-${GOOS}-${GOARCH}.tar.gz" \ -C build/c codebase-memory-mcp LICENSE install.sh - name: Build UI binary - run: scripts/build.sh --with-ui ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=${{ matrix.cc }} CXX=${{ matrix.cxx }} + env: + VERSION: ${{ inputs.version }} + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} + run: | + if [ -n "$VERSION" ]; then + scripts/build.sh --with-ui --version "$VERSION" "CC=$CC" "CXX=$CXX" + else + scripts/build.sh --with-ui "CC=$CC" "CXX=$CXX" + fi - name: Ad-hoc sign macOS UI binary if: startsWith(matrix.os, 'macos') @@ -76,9 +97,12 @@ jobs: run: scripts/security-ui.sh - name: Archive UI binary + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} run: | cp LICENSE install.sh build/c/ - tar -czf codebase-memory-mcp-ui-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \ + tar -czf "codebase-memory-mcp-ui-${GOOS}-${GOARCH}.tar.gz" \ -C build/c codebase-memory-mcp LICENSE install.sh - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 @@ -108,7 +132,14 @@ jobs: - name: Build standard binary shell: msys2 {0} - run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=clang CXX=clang++ + env: + VERSION: ${{ inputs.version }} + run: | + if [ -n "$VERSION" ]; then + scripts/build.sh --version "$VERSION" CC=clang CXX=clang++ + else + scripts/build.sh CC=clang CXX=clang++ + fi - name: Archive standard binary shell: msys2 {0} @@ -120,7 +151,14 @@ jobs: - name: Build UI binary shell: msys2 {0} - run: scripts/build.sh --with-ui ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=clang CXX=clang++ + env: + VERSION: ${{ inputs.version }} + run: | + if [ -n "$VERSION" ]; then + scripts/build.sh --with-ui --version "$VERSION" CC=clang CXX=clang++ + else + scripts/build.sh --with-ui CC=clang CXX=clang++ + fi - name: Archive UI binary shell: msys2 {0} @@ -159,7 +197,14 @@ jobs: node-version: "22" - name: Build standard binary (static) - run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=gcc CXX=g++ STATIC=1 + env: + VERSION: ${{ inputs.version }} + run: | + if [ -n "$VERSION" ]; then + scripts/build.sh --version "$VERSION" CC=gcc CXX=g++ STATIC=1 + else + scripts/build.sh CC=gcc CXX=g++ STATIC=1 + fi - name: Verify static linking run: | @@ -167,18 +212,29 @@ jobs: ldd build/c/codebase-memory-mcp 2>&1 | grep -q "not a dynamic executable" || ldd build/c/codebase-memory-mcp 2>&1 | grep -q "statically linked" - name: Archive standard binary + env: + ARCH: ${{ matrix.arch }} run: | cp LICENSE install.sh build/c/ - tar -czf codebase-memory-mcp-linux-${{ matrix.arch }}-portable.tar.gz \ + tar -czf "codebase-memory-mcp-linux-${ARCH}-portable.tar.gz" \ -C build/c codebase-memory-mcp LICENSE install.sh - name: Build UI binary (static) - run: scripts/build.sh --with-ui ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=gcc CXX=g++ STATIC=1 + env: + VERSION: ${{ inputs.version }} + run: | + if [ -n "$VERSION" ]; then + scripts/build.sh --with-ui --version "$VERSION" CC=gcc CXX=g++ STATIC=1 + else + scripts/build.sh --with-ui CC=gcc CXX=g++ STATIC=1 + fi - name: Archive UI binary + env: + ARCH: ${{ matrix.arch }} run: | cp LICENSE install.sh build/c/ - tar -czf codebase-memory-mcp-ui-linux-${{ matrix.arch }}-portable.tar.gz \ + tar -czf "codebase-memory-mcp-ui-linux-${ARCH}-portable.tar.gz" \ -C build/c codebase-memory-mcp LICENSE install.sh - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0