fix(csrf): reject null Origin header on state-changing API requests #3673
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Issue Commands | |
| # Single source of truth for all issue comment commands: | |
| # /assign → contributor requests assignment | |
| # /assign @user → maintainer assigns a user | |
| # /duplicate #N → maintainer marks duplicate | |
| # /already-implemented → maintainer marks already implemented | |
| # Also handles label triggers (duplicate, already-implemented) and cleanup. | |
| on: | |
| issue_comment: | |
| types: [created] | |
| issues: | |
| types: [assigned, labeled] | |
| permissions: {} | |
| jobs: | |
| # ── Contributor requests assignment (/assign without @user) ────────────────── | |
| contributor-request: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| if: | | |
| github.event_name == 'issue_comment' && | |
| !github.event.issue.pull_request && | |
| (contains(github.event.comment.body, 'assign') || contains(github.event.comment.body, 'Assign') || contains(github.event.comment.body, '/assign')) && | |
| !contains(github.event.comment.body, '/assign @') && | |
| !contains(github.event.comment.body, '/duplicate') && | |
| !contains(github.event.comment.body, '/already-implemented') | |
| steps: | |
| - name: Acknowledge assignment request (idempotent) | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const commenter = context.payload.comment.user.login; | |
| const issueNumber = context.issue.number; | |
| // Skip if commenter is a maintainer | |
| try { | |
| const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| username: commenter, | |
| }); | |
| if (['admin', 'write'].includes(perm.permission)) { | |
| console.log(`${commenter} is a maintainer — skipping contributor flow.`); | |
| return; | |
| } | |
| } catch (e) { | |
| console.log('Could not fetch permission level:', e.message); | |
| } | |
| // Check if already assigned to someone | |
| const { data: issue } = await github.rest.issues.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| }); | |
| if (issue.assignees && issue.assignees.length > 0) { | |
| const assigneeNames = issue.assignees.map(a => `@${a.login}`).join(', '); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| body: `Hi @${commenter}, this issue is already assigned to ${assigneeNames}. Please look for other open issues to work on! 🙏`, | |
| }); | |
| return; | |
| } | |
| // Check idempotency: don't post duplicate ack if we already replied to this user | |
| const marker = `<!-- assign-ack-${commenter} -->`; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| }); | |
| if (comments.some(c => c.body.includes(marker))) { | |
| console.log(`Already acknowledged assignment request from ${commenter}.`); | |
| return; | |
| } | |
| // Add needs-maintainer-approval label | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| labels: ['needs-maintainer-approval'], | |
| }); | |
| } catch (e) { | |
| console.log('Could not add label:', e.message); | |
| } | |
| // Post acknowledgement | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| body: `${marker}\nHello @${commenter}! 👋 Thanks for your interest in working on this issue.\n\nWe have automatically notified the maintainers for you. They will review your request and officially assign the issue to you shortly if it's available.\n\n**Note:** Please do not start working or raise a pull request until you are officially assigned!`, | |
| }); | |
| # ── Maintainer assigns user (/assign @username) ───────────────────────────── | |
| maintainer-assign: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| if: | | |
| github.event_name == 'issue_comment' && | |
| !github.event.issue.pull_request && | |
| contains(github.event.comment.body, '/assign @') | |
| steps: | |
| - name: Assign user via maintainer command | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const commenter = context.payload.comment.user.login; | |
| // Verify maintainer | |
| try { | |
| const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| username: commenter, | |
| }); | |
| if (!['admin', 'write'].includes(perm.permission)) { | |
| console.log(`${commenter} is not a maintainer.`); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: `@${commenter} Only maintainers can use the \`/assign @user\` command.`, | |
| }); | |
| return; | |
| } | |
| } catch (e) { | |
| return; | |
| } | |
| const match = context.payload.comment.body.match(/\/assign\s+@([a-zA-Z0-9_-]+)/); | |
| if (!match) return; | |
| const assignee = match[1]; | |
| try { | |
| await github.rest.issues.addAssignees({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| assignees: [assignee], | |
| }); | |
| } catch (e) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: `❌ Could not assign @${assignee}: ${e.message}`, | |
| }); | |
| return; | |
| } | |
| // Remove approval label | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| name: 'needs-maintainer-approval', | |
| }); | |
| } catch (e) { /* label may not be present */ } | |
| // Removed success comment here as cleanup-on-assign will now handle notifying the user. | |
| # ── Maintainer closes issue as duplicate or already-implemented ──────────────────────── | |
| maintainer-close-issue: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| if: | | |
| (github.event_name == 'issue_comment' && !github.event.issue.pull_request && | |
| (contains(github.event.comment.body, '/duplicate') || contains(github.event.comment.body, '/already-implemented'))) || | |
| (github.event_name == 'issues' && github.event.action == 'labeled' && | |
| (github.event.label.name == 'duplicate' || github.event.label.name == 'already-implemented')) | |
| steps: | |
| - name: Process duplicate or already-implemented close | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const commenter = context.payload.sender.login; | |
| // Verify maintainer for comment events | |
| if (context.eventName === 'issue_comment') { | |
| try { | |
| const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| username: commenter, | |
| }); | |
| if (!['admin', 'write'].includes(perm.permission)) { | |
| console.log(`${commenter} is not a maintainer.`); | |
| return; | |
| } | |
| } catch (e) { | |
| return; | |
| } | |
| } | |
| let targetLabel = null; | |
| let closeReason = 'not_planned'; | |
| let commentBody = ''; | |
| if (context.eventName === 'issue_comment') { | |
| const body = context.payload.comment.body; | |
| if (body.includes('/duplicate')) { | |
| const match = body.match(/\/duplicate\s+#(\d+)/); | |
| const dupNumber = match ? match[1] : ''; | |
| targetLabel = 'duplicate'; | |
| commentBody = dupNumber | |
| ? `This issue has been marked as a **duplicate** of #${dupNumber}. Closing to keep our tracker organized. Please search existing issues before opening new ones!` | |
| : `This issue has been marked as a **duplicate**. Closing to keep our tracker organized. Please search existing issues before opening new ones!`; | |
| } else if (body.includes('/already-implemented')) { | |
| targetLabel = 'already-implemented'; | |
| commentBody = `This issue has been closed because the requested features or bug fixes are already implemented in the repository.`; | |
| } | |
| } else if (context.eventName === 'issues' && context.payload.action === 'labeled') { | |
| const labelName = context.payload.label.name; | |
| if (labelName === 'duplicate') { | |
| targetLabel = 'duplicate'; | |
| commentBody = `This issue has been marked as a **duplicate**. Closing to keep our tracker organized. Please search existing issues before opening new ones!`; | |
| } else if (labelName === 'already-implemented') { | |
| targetLabel = 'already-implemented'; | |
| commentBody = `This issue has been closed because the requested features or bug fixes are already implemented in the repository.`; | |
| } | |
| } | |
| if (targetLabel) { | |
| // 1. Ensure target label exists | |
| try { | |
| await github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: targetLabel, | |
| color: targetLabel === 'duplicate' ? 'cfd3d7' : '0e8a16', | |
| description: targetLabel === 'duplicate' ? 'This issue is a duplicate' : 'This issue is already implemented', | |
| }); | |
| } catch (e) { | |
| // Ignore if it already exists | |
| } | |
| // 2. Set labels (clears other labels) | |
| await github.rest.issues.setLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| labels: [targetLabel], | |
| }); | |
| // 3. Post comment if new | |
| if (commentBody) { | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const alreadyCommented = comments.some(c => c.body.includes(commentBody.substring(0, 30))); | |
| if (!alreadyCommented) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: commentBody, | |
| }); | |
| } | |
| } | |
| // 4. Close issue | |
| await github.rest.issues.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| state: 'closed', | |
| state_reason: closeReason, | |
| }); | |
| } | |
| # ── Issue assigned → remove approval label ────────────────────────────────── | |
| cleanup-on-assign: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| if: github.event_name == 'issues' && github.event.action == 'assigned' | |
| steps: | |
| - name: Remove needs-maintainer-approval label | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| name: 'needs-maintainer-approval', | |
| }); | |
| console.log('Removed needs-maintainer-approval label.'); | |
| } catch (e) { | |
| console.log('Label already removed or not present.'); | |
| } | |
| // Post assignment notification | |
| if (context.payload.assignee) { | |
| const assignee = context.payload.assignee.login; | |
| const marker = `<!-- assignment-notification-${assignee} -->`; | |
| try { | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| if (!comments.some(c => c.body && c.body.includes(marker))) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: `${marker}\n✅ Hi @${assignee}, you have been assigned to this issue! You have 7 days to work on this issue and create a PR. Happy coding! 🚀`, | |
| }); | |
| console.log(`Posted assignment notification for @${assignee}`); | |
| } | |
| } catch (e) { | |
| console.log(`Failed to post notification for @${assignee}: ${e.message}`); | |
| } | |
| } |