# 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');