unsafe-pr-checkout-helper.ts 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. import * as github from '@actions/github'
  2. import {fromPayload} from './ref-helper.js'
  3. const PR_REF_PATTERN = /^refs\/pull\/[0-9]+\/(?:head|merge)$/
  4. export interface IUnsafePrCheckoutInput {
  5. qualifiedRepository: string
  6. ref: string
  7. commit: string | undefined
  8. allowUnsafePrCheckout: boolean
  9. }
  10. export function assertSafePrCheckout(input: IUnsafePrCheckoutInput): void {
  11. if (input.allowUnsafePrCheckout) {
  12. return
  13. }
  14. const eventName = github.context.eventName
  15. if (eventName !== 'pull_request_target' && eventName !== 'workflow_run') {
  16. return
  17. }
  18. const baseRepoId = fromPayload('repository.id')
  19. if (typeof baseRepoId !== 'number') {
  20. return
  21. }
  22. let prHeadRepoId: unknown
  23. let prHeadRepoFullName: unknown
  24. const prShas: string[] = []
  25. if (eventName === 'pull_request_target') {
  26. prHeadRepoId = fromPayload('pull_request.head.repo.id')
  27. prHeadRepoFullName = fromPayload('pull_request.head.repo.full_name')
  28. pushIfSha(prShas, fromPayload('pull_request.head.sha'))
  29. pushIfSha(prShas, fromPayload('pull_request.merge_commit_sha'))
  30. } else {
  31. const wrEvent = fromPayload('workflow_run.event')
  32. if (typeof wrEvent !== 'string' || !wrEvent.startsWith('pull_request')) {
  33. return
  34. }
  35. prHeadRepoId = fromPayload('workflow_run.head_repository.id')
  36. prHeadRepoFullName = fromPayload('workflow_run.head_repository.full_name')
  37. pushIfSha(prShas, fromPayload('workflow_run.head_commit.id'))
  38. // For `pull_request_target`-triggered workflow_run, `head_sha` is the base
  39. // default branch SHA (not the PR head)
  40. if (wrEvent !== 'pull_request_target') {
  41. pushIfSha(prShas, fromPayload('workflow_run.head_sha'))
  42. }
  43. }
  44. // (A) Fork PR?
  45. if (typeof prHeadRepoId !== 'number' || prHeadRepoId === baseRepoId) {
  46. return
  47. }
  48. // (B) We cannot check for all fork PR refs so check to see
  49. // if the resolved input points to the fork PR sha we have in the payload
  50. const repositoryMatchesPrHead =
  51. typeof prHeadRepoFullName === 'string' &&
  52. input.qualifiedRepository.toLowerCase() === prHeadRepoFullName.toLowerCase()
  53. const refMatchesPullPattern = PR_REF_PATTERN.test(input.ref)
  54. const commitMatchesPrHeadSha =
  55. !!input.commit && prShas.includes(input.commit.toLowerCase())
  56. if (
  57. !repositoryMatchesPrHead &&
  58. !refMatchesPullPattern &&
  59. !commitMatchesPrHeadSha
  60. ) {
  61. return
  62. }
  63. throw new Error(
  64. `Refusing to check out fork pull request code from a '${eventName}' workflow. ` +
  65. `This workflow runs with the base repository's GITHUB_TOKEN, secrets, default-branch ` +
  66. `cache scope, and runner access. Fetching and executing a fork's code in that trusted ` +
  67. `context commonly leads to "pwn request" vulnerabilities. To opt in, review the risks ` +
  68. `at https://gh.io/securely-using-pull_request_target and set 'allow-unsafe-pr-checkout: true' ` +
  69. `on the actions/checkout step.`
  70. )
  71. }
  72. function pushIfSha(target: string[], value: unknown): void {
  73. if (typeof value === 'string' && value.length > 0) {
  74. target.push(value.toLowerCase())
  75. }
  76. }