From 1f554c26150020c0c5b77b9aa1f3b5a5c34f0feb Mon Sep 17 00:00:00 2001 From: GAdityaVarma Date: Wed, 8 Apr 2026 14:21:51 +0530 Subject: [PATCH 1/5] SECCMP-1797: Fix PwnRequest shell injection in jira-id-check Move all attacker-controlled inputs (pr-title, github.event.pull_request.title) from direct ${{ }} interpolation in shell run blocks to safe env: variables. This eliminates the script injection vector where a malicious PR title could execute arbitrary commands with the workflow's write token. Also adds explicit permissions: pull-requests: read to enforce least privilege. --- .github/workflows/jira-id-check.yml | 53 +++++++++++++++++------------ 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/.github/workflows/jira-id-check.yml b/.github/workflows/jira-id-check.yml index a290f58..d4d88c6 100644 --- a/.github/workflows/jira-id-check.yml +++ b/.github/workflows/jira-id-check.yml @@ -40,68 +40,77 @@ on: jobs: check-jira-id: runs-on: ubuntu-latest + permissions: + pull-requests: read steps: - name: 🏷️ Validate JIRA ticket ID in PR title shell: bash env: GH_TOKEN: ${{ github.token }} + INPUT_PR_TITLE: ${{ inputs.pr-title }} + EVENT_PR_TITLE: ${{ github.event.pull_request.title }} + PR_NUMBER: ${{ github.event.pull_request.number }} + GH_REPO: ${{ github.repository }} + INPUT_REGEX_PATTERN: ${{ inputs.regex-pattern }} + INPUT_FAIL_IF_NO_JIRA_ID: ${{ inputs.fail-if-no-jira-id }} + INPUT_ALLOW_WIP: ${{ inputs.allow-wip }} + INPUT_CASE_SENSITIVE: ${{ inputs.case-sensitive }} run: | # Get PR title from context or input parameter (for pull_request_target support) - if [[ -n "${{ inputs.pr-title }}" ]]; then - PR_TITLE="${{ inputs.pr-title }}" + if [[ -n "$INPUT_PR_TITLE" ]]; then + PR_TITLE="$INPUT_PR_TITLE" echo "Using PR title from input parameter" else - PR_TITLE="${{ github.event.pull_request.title }}" + PR_TITLE="$EVENT_PR_TITLE" echo "Using PR title from GitHub event context" fi - + # Fetch the current PR title from the API to handle re-runs correctly. # When a workflow is re-run (either directly or via a reusable workflow caller), # the event payload contains the title from the original trigger, not the current # state. If the user edited the title (e.g., to add a JIRA ID), we need to detect # that and use the updated title instead. - PR_NUMBER="${{ github.event.pull_request.number }}" if [[ -n "$PR_NUMBER" ]]; then LIVE_TITLE=$(gh pr view "$PR_NUMBER" \ - --repo "${{ github.repository }}" \ + --repo "$GH_REPO" \ --json title --jq '.title' 2>/dev/null || true) - + if [[ -n "$LIVE_TITLE" && "$LIVE_TITLE" != "$PR_TITLE" ]]; then - echo "⚠️ PR title was updated since workflow was triggered" - echo " Event title: $PR_TITLE" - echo " Current title: $LIVE_TITLE" + echo "PR title was updated since workflow was triggered" + echo " Event title: $PR_TITLE" + echo " Current title: $LIVE_TITLE" PR_TITLE="$LIVE_TITLE" fi fi echo "PR Title: $PR_TITLE" - + # Set up inputs as environment variables # JIRA project keys are defined at the organization level - JIRA_PROJECT_KEYS="${{ env.ORGANIZATION_JIRA_PROJECT_KEYS }}" - REGEX_PATTERN="${{ inputs.regex-pattern }}" - FAIL_IF_NO_JIRA_ID="${{ inputs.fail-if-no-jira-id }}" - ALLOW_WIP="${{ inputs.allow-wip }}" - CASE_SENSITIVE="${{ inputs.case-sensitive }}" - + JIRA_PROJECT_KEYS="$ORGANIZATION_JIRA_PROJECT_KEYS" + REGEX_PATTERN="$INPUT_REGEX_PATTERN" + FAIL_IF_NO_JIRA_ID="$INPUT_FAIL_IF_NO_JIRA_ID" + ALLOW_WIP="$INPUT_ALLOW_WIP" + CASE_SENSITIVE="$INPUT_CASE_SENSITIVE" + echo "Using organization-wide JIRA project keys: $JIRA_PROJECT_KEYS" echo "Using regex pattern: $REGEX_PATTERN" echo "Fail if no JIRA ID: $FAIL_IF_NO_JIRA_ID" echo "Allow WIP PRs: $ALLOW_WIP" echo "Case sensitive: $CASE_SENSITIVE" - + # Handle WIP PRs if [[ "$ALLOW_WIP" == "true" && "${PR_TITLE,,}" =~ ^wip: ]]; then echo "This is a WIP PR. Skipping JIRA ID check." exit 0 fi - + # Convert comma-separated project keys to array IFS=',' read -ra PROJECT_KEYS <<< "$JIRA_PROJECT_KEYS" echo "Valid project keys: ${PROJECT_KEYS[*]}" - + # Directly check for valid JIRA IDs in the PR title VALID_ID_FOUND=false - + for VALID_KEY in "${PROJECT_KEYS[@]}"; do # Create a pattern specifically for this project key if [[ "$CASE_SENSITIVE" == "true" ]]; then @@ -122,7 +131,7 @@ jobs: fi fi done - + if [[ "$VALID_ID_FOUND" != "true" ]]; then echo "ERROR: No JIRA ID found in PR title: \"$PR_TITLE\". Valid project keys are: $JIRA_PROJECT_KEYS" echo "::error::No JIRA ID found in PR title: \"$PR_TITLE\". Valid project keys are: $JIRA_PROJECT_KEYS" From 902c35521308e3150be6caa7ca849c801dc2eac7 Mon Sep 17 00:00:00 2001 From: GAdityaVarma Date: Wed, 8 Apr 2026 14:30:07 +0530 Subject: [PATCH 2/5] SECCMP-1797: Pin trufflehog action and image to v3.94.2 Replace mutable @main tag with commit SHA 6bd2d14f for the GitHub Action, and :latest with :3.94.2 for the Docker image. Using @main or :latest means any commit pushed to the trufflehog repo takes effect immediately in our workflows - a supply chain risk. --- .github/workflows/trufflehog-scan.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/trufflehog-scan.yml b/.github/workflows/trufflehog-scan.yml index 6f04b7e..566ea9c 100644 --- a/.github/workflows/trufflehog-scan.yml +++ b/.github/workflows/trufflehog-scan.yml @@ -70,7 +70,8 @@ jobs: - name: TruffleHog Scan id: trufflehog - uses: trufflesecurity/trufflehog@main + # Pinned to v3.94.2 commit SHA to prevent supply chain attacks via mutable tag + uses: trufflesecurity/trufflehog@6bd2d14f7a4bc1e569fa3550efa7ec632a4fa67b # v3.94.2 continue-on-error: true with: base: ${{ github.event.pull_request.base.sha }} @@ -107,8 +108,9 @@ jobs: echo "$CHANGED_FILES" # Scan only the changed files in their current state using filesystem scanner + # Pinned to v3.94.2 to match the action version above SCAN_OUTPUT=$(docker run --rm -v "$(pwd)":/tmp -w /tmp \ - ghcr.io/trufflesecurity/trufflehog:latest \ + ghcr.io/trufflesecurity/trufflehog:3.94.2 \ filesystem /tmp/ \ --json \ ${{ steps.config.outputs.exclude_args }} \ @@ -266,7 +268,7 @@ jobs: ### Understanding Results | Type | Meaning | Action Required | - |------|---------|-----------------| + |------|---------|--------------------| | **Verified** | Confirmed active credential | **Must remove & rotate** - PR blocked | | **Unverified** | Potential secret pattern | Review recommended - PR can proceed | From 00e66d51d67860f1b832bf9bd584332ebbebed60 Mon Sep 17 00:00:00 2001 From: GAdityaVarma Date: Wed, 8 Apr 2026 15:35:35 +0530 Subject: [PATCH 3/5] SECCMP-1797: Remove unused regex-pattern and fail-if-no-jira-id inputs These inputs were defined but never wired into the matching logic: - regex-pattern: matching always used hardcoded VALID_KEY-[0-9]+ - fail-if-no-jira-id: script always exits 1 when no ID found Removed the inputs, their env: mappings, local variable assignments, and debug logging to avoid misleading callers. No caller passes these inputs today. --- .github/workflows/jira-id-check.yml | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.github/workflows/jira-id-check.yml b/.github/workflows/jira-id-check.yml index d4d88c6..6b1ab8c 100644 --- a/.github/workflows/jira-id-check.yml +++ b/.github/workflows/jira-id-check.yml @@ -16,16 +16,6 @@ on: required: false type: string description: 'The PR title to check (for pull_request_target support)' - regex-pattern: - required: false - type: string - description: 'Custom regex pattern to match JIRA IDs (defaults to "[A-Z]+-[0-9]+")' - default: '[A-Z]+-[0-9]+' - fail-if-no-jira-id: - required: false - type: string - description: 'Whether to fail the check if no JIRA ID is found' - default: 'true' allow-wip: required: false type: string @@ -51,8 +41,6 @@ jobs: EVENT_PR_TITLE: ${{ github.event.pull_request.title }} PR_NUMBER: ${{ github.event.pull_request.number }} GH_REPO: ${{ github.repository }} - INPUT_REGEX_PATTERN: ${{ inputs.regex-pattern }} - INPUT_FAIL_IF_NO_JIRA_ID: ${{ inputs.fail-if-no-jira-id }} INPUT_ALLOW_WIP: ${{ inputs.allow-wip }} INPUT_CASE_SENSITIVE: ${{ inputs.case-sensitive }} run: | @@ -84,17 +72,11 @@ jobs: fi echo "PR Title: $PR_TITLE" - # Set up inputs as environment variables - # JIRA project keys are defined at the organization level JIRA_PROJECT_KEYS="$ORGANIZATION_JIRA_PROJECT_KEYS" - REGEX_PATTERN="$INPUT_REGEX_PATTERN" - FAIL_IF_NO_JIRA_ID="$INPUT_FAIL_IF_NO_JIRA_ID" ALLOW_WIP="$INPUT_ALLOW_WIP" CASE_SENSITIVE="$INPUT_CASE_SENSITIVE" echo "Using organization-wide JIRA project keys: $JIRA_PROJECT_KEYS" - echo "Using regex pattern: $REGEX_PATTERN" - echo "Fail if no JIRA ID: $FAIL_IF_NO_JIRA_ID" echo "Allow WIP PRs: $ALLOW_WIP" echo "Case sensitive: $CASE_SENSITIVE" From e7f554c103b6000c2f776f76addf4b2a2470466e Mon Sep 17 00:00:00 2001 From: GAdityaVarma Date: Wed, 8 Apr 2026 19:23:19 +0530 Subject: [PATCH 4/5] SECCMP-1797: Add persist-credentials: false to copyright-check checkouts Prevents the write token from being persisted to disk (.git/config) after checkout. This limits exposure if attacker-controlled PR code could read files from the runner filesystem. --- .github/workflows/copyright-check.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index e1c72c9..aee14b4 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -18,6 +18,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} path: target-repo + persist-credentials: false - name: Checkout pr-workflows repo uses: actions/checkout@v4 @@ -25,6 +26,7 @@ jobs: repository: ${{ github.repository_owner }}/pr-workflows ref: main path: pr-workflows + persist-credentials: false - name: Setup config id: setup-config @@ -135,4 +137,4 @@ jobs: - name: No-op summary if: steps.changed-files.outputs.skip-validation == 'true' - run: echo "::notice title=Copyright Check::No files to validate" \ No newline at end of file + run: echo "::notice title=Copyright Check::No files to validate" From 9326a7c89cb69d04ac5f2fec63aaa4e4277605d2 Mon Sep 17 00:00:00 2001 From: GAdityaVarma Date: Wed, 8 Apr 2026 22:14:08 +0530 Subject: [PATCH 5/5] fix: move remaining ${{ }} expressions from shell run: to env: blocks P1 fixes: - copyright-check.yml: move github.event.pull_request.base.ref to env: (branch names can contain shell metacharacters - injection risk) - trufflehog-scan.yml: add persist-credentials: false to checkout - trufflehog-scan.yml: move vars.TRUFFLEHOG_EXCLUDES to env: P2 fixes (defense-in-depth): - trufflehog-scan.yml: move step output counts to env: in Process step - copyright-check.yml: move step outputs (config-file, status) to env: - copyright-check.yml: move base.sha, head.sha, base.ref to env: in Get changed files step --- .github/workflows/copyright-check.yml | 24 ++++++++++++++++-------- .github/workflows/trufflehog-scan.yml | 14 ++++++++++---- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index aee14b4..e290f8e 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -30,11 +30,15 @@ jobs: - name: Setup config id: setup-config + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} run: | cfg="target-repo/.copyrightconfig" if [ ! -f "$cfg" ]; then - git clone --depth 1 --branch ${{ github.event.pull_request.base.ref }} \ - https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git base-repo + git clone --depth 1 --branch "$BASE_REF" \ + https://x-access-token:${GH_TOKEN}@github.com/${GH_REPO}.git base-repo [ -f base-repo/.copyrightconfig ] && cfg="base-repo/.copyrightconfig" fi [ -f "$cfg" ] || { echo "missing config"; exit 1; } @@ -49,12 +53,14 @@ jobs: - name: Get changed files id: changed-files + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + BASE_REF: ${{ github.event.pull_request.base.ref }} run: | cd target-repo - base_sha="${{ github.event.pull_request.base.sha }}" - head_sha="${{ github.event.pull_request.head.sha }}" - git fetch origin ${{ github.event.pull_request.base.ref }} - git diff --name-only --diff-filter=AMR "$base_sha" "$head_sha" | while read f; do [ -f "$f" ] && echo "$f"; done > ../files_to_check.txt + git fetch origin "$BASE_REF" + git diff --name-only --diff-filter=AMR "$BASE_SHA" "$HEAD_SHA" | while read f; do [ -f "$f" ] && echo "$f"; done > ../files_to_check.txt count=$(wc -l < ../files_to_check.txt | tr -d ' ') if [ "$count" -eq 0 ]; then echo "skip-validation=true" >> $GITHUB_OUTPUT; else echo "skip-validation=false" >> $GITHUB_OUTPUT; fi echo "files-count=$count" >> $GITHUB_OUTPUT @@ -65,9 +71,10 @@ jobs: continue-on-error: true env: COPYRIGHT_CHECK_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} + CONFIG_FILE: ${{ steps.setup-config.outputs.config-file }} run: | script="pr-workflows/scripts/copyrightcheck.py" - cfg="${{ steps.setup-config.outputs.config-file }}" + cfg="$CONFIG_FILE" [ -f "$script" ] || { echo "script missing"; exit 1; } chmod +x "$script" files=$(tr '\n' ' ' < files_to_check.txt) @@ -125,8 +132,9 @@ jobs: if: always() && steps.changed-files.outputs.skip-validation != 'true' env: COMMENT_ACTION: ${{ steps.pr-comment.outputs.comment_action }} + VALIDATION_STATUS: ${{ steps.validate.outputs.status }} run: | - if [ "${{ steps.validate.outputs.status }}" != "success" ]; then + if [ "$VALIDATION_STATUS" != "success" ]; then if [ "$COMMENT_ACTION" = "updated" ] || [ "$COMMENT_ACTION" = "created" ]; then echo "::error title=Copyright Validation Failed::See the $COMMENT_ACTION PR comment for detailed results."; else diff --git a/.github/workflows/trufflehog-scan.yml b/.github/workflows/trufflehog-scan.yml index 566ea9c..8999672 100644 --- a/.github/workflows/trufflehog-scan.yml +++ b/.github/workflows/trufflehog-scan.yml @@ -40,6 +40,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + persist-credentials: false - name: Fetch PR head commits if: github.event_name != 'workflow_dispatch' @@ -50,6 +51,8 @@ jobs: - name: Setup exclude config id: config + env: + TRUFFLEHOG_EXCLUDES: ${{ vars.TRUFFLEHOG_EXCLUDES }} run: | # Always include default exclusions echo "Adding default exclusions" @@ -58,10 +61,10 @@ jobs: EOF # Append repo/org-level custom exclusions if defined - if [ -n "${{ vars.TRUFFLEHOG_EXCLUDES }}" ]; then + if [ -n "$TRUFFLEHOG_EXCLUDES" ]; then echo "Adding repo/org-level TRUFFLEHOG_EXCLUDES patterns" # Support both comma-separated and newline-separated patterns - echo "${{ vars.TRUFFLEHOG_EXCLUDES }}" | tr ',' '\n' | sed '/^$/d' >> .trufflehog-ignore + echo "$TRUFFLEHOG_EXCLUDES" | tr ',' '\n' | sed '/^$/d' >> .trufflehog-ignore fi echo "Exclusion patterns:" @@ -156,9 +159,12 @@ jobs: - name: Process scan results id: process if: github.event_name != 'workflow_dispatch' + env: + VERIFIED_COUNT: ${{ steps.parse.outputs.verified_count }} + UNVERIFIED_COUNT: ${{ steps.parse.outputs.unverified_count }} run: | - VERIFIED=${{ steps.parse.outputs.verified_count || 0 }} - UNVERIFIED=${{ steps.parse.outputs.unverified_count || 0 }} + VERIFIED=${VERIFIED_COUNT:-0} + UNVERIFIED=${UNVERIFIED_COUNT:-0} if [ "$VERIFIED" -gt 0 ]; then # Verified secrets found - must fail