unsafe-pr-checkout-helper.test.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import * as github from '@actions/github'
  2. import {assertSafePrCheckout} from '../lib/unsafe-pr-checkout-helper'
  3. // Shallow clone original @actions/github context
  4. const originalContext = {...github.context}
  5. const originalEventName = github.context.eventName
  6. const originalPayload = github.context.payload
  7. const BASE_REPO_ID = 100
  8. const FORK_REPO_ID = 200
  9. const PR_HEAD_SHA = '1111111111111111111111111111111111111111'
  10. const PR_MERGE_SHA = '2222222222222222222222222222222222222222'
  11. const SAFE_BASE_SHA = '3333333333333333333333333333333333333333'
  12. const WORKFLOW_RUN_HEAD_COMMIT_SHA = '4444444444444444444444444444444444444444'
  13. const BASE_QUALIFIED_REPO = 'some-owner/some-repo'
  14. const FORK_QUALIFIED_REPO = 'another-repo/fork'
  15. function setContext(eventName: string, payload: object): void {
  16. ;(github.context as {eventName: string}).eventName = eventName
  17. ;(github.context as {payload: object}).payload = payload
  18. }
  19. function forkPullRequestTargetPayload(): object {
  20. return {
  21. repository: {id: BASE_REPO_ID},
  22. pull_request: {
  23. head: {
  24. sha: PR_HEAD_SHA,
  25. repo: {id: FORK_REPO_ID, full_name: FORK_QUALIFIED_REPO}
  26. },
  27. merge_commit_sha: PR_MERGE_SHA
  28. }
  29. }
  30. }
  31. function sameRepoPullRequestTargetPayload(): object {
  32. return {
  33. repository: {id: BASE_REPO_ID},
  34. pull_request: {
  35. head: {
  36. sha: PR_HEAD_SHA,
  37. repo: {id: BASE_REPO_ID, full_name: BASE_QUALIFIED_REPO}
  38. },
  39. merge_commit_sha: PR_MERGE_SHA
  40. }
  41. }
  42. }
  43. function forkWorkflowRunPayload(): object {
  44. return {
  45. repository: {id: BASE_REPO_ID},
  46. workflow_run: {
  47. event: 'pull_request',
  48. head_commit: {id: WORKFLOW_RUN_HEAD_COMMIT_SHA},
  49. head_repository: {id: FORK_REPO_ID, full_name: FORK_QUALIFIED_REPO}
  50. }
  51. }
  52. }
  53. describe('unsafe-pr-checkout-helper', () => {
  54. beforeAll(() => {
  55. jest.spyOn(github.context, 'repo', 'get').mockReturnValue({
  56. owner: 'some-owner',
  57. repo: 'some-repo'
  58. })
  59. })
  60. afterEach(() => {
  61. ;(github.context as {eventName: string}).eventName = originalEventName
  62. ;(github.context as {payload: object}).payload = originalPayload
  63. })
  64. afterAll(() => {
  65. ;(github.context as {eventName: string}).eventName =
  66. originalContext.eventName
  67. ;(github.context as {payload: object}).payload = originalContext.payload
  68. jest.restoreAllMocks()
  69. })
  70. it('allows pull_request events untouched', () => {
  71. setContext('pull_request', forkPullRequestTargetPayload())
  72. expect(() =>
  73. assertSafePrCheckout({
  74. qualifiedRepository: 'attacker/fork',
  75. ref: 'refs/pull/1/merge',
  76. commit: '',
  77. allowUnsafePrCheckout: false
  78. })
  79. ).not.toThrow()
  80. })
  81. it('allows pull_request_target default checkout (base branch)', () => {
  82. setContext('pull_request_target', forkPullRequestTargetPayload())
  83. expect(() =>
  84. assertSafePrCheckout({
  85. qualifiedRepository: BASE_QUALIFIED_REPO,
  86. ref: 'refs/heads/main',
  87. commit: SAFE_BASE_SHA,
  88. allowUnsafePrCheckout: false
  89. })
  90. ).not.toThrow()
  91. })
  92. it('allows same-repo pull_request_target checkout of PR head', () => {
  93. setContext('pull_request_target', sameRepoPullRequestTargetPayload())
  94. expect(() =>
  95. assertSafePrCheckout({
  96. qualifiedRepository: BASE_QUALIFIED_REPO,
  97. ref: '',
  98. commit: PR_HEAD_SHA,
  99. allowUnsafePrCheckout: false
  100. })
  101. ).not.toThrow()
  102. })
  103. it('refuses pull_request_target fork PR head SHA checkout', () => {
  104. setContext('pull_request_target', forkPullRequestTargetPayload())
  105. expect(() =>
  106. assertSafePrCheckout({
  107. qualifiedRepository: BASE_QUALIFIED_REPO,
  108. ref: '',
  109. commit: PR_HEAD_SHA,
  110. allowUnsafePrCheckout: false
  111. })
  112. ).toThrow(/Refusing to check out fork pull request code/)
  113. })
  114. it('refuses pull_request_target fork PR merge_commit_sha checkout', () => {
  115. setContext('pull_request_target', forkPullRequestTargetPayload())
  116. expect(() =>
  117. assertSafePrCheckout({
  118. qualifiedRepository: BASE_QUALIFIED_REPO,
  119. ref: '',
  120. commit: PR_MERGE_SHA,
  121. allowUnsafePrCheckout: false
  122. })
  123. ).toThrow(/allow-unsafe-pr-checkout/)
  124. })
  125. it('refuses pull_request_target fork PR ref pattern (head)', () => {
  126. setContext('pull_request_target', forkPullRequestTargetPayload())
  127. expect(() =>
  128. assertSafePrCheckout({
  129. qualifiedRepository: BASE_QUALIFIED_REPO,
  130. ref: 'refs/pull/42/head',
  131. commit: '',
  132. allowUnsafePrCheckout: false
  133. })
  134. ).toThrow()
  135. })
  136. it('refuses pull_request_target fork PR ref pattern (merge)', () => {
  137. setContext('pull_request_target', forkPullRequestTargetPayload())
  138. expect(() =>
  139. assertSafePrCheckout({
  140. qualifiedRepository: BASE_QUALIFIED_REPO,
  141. ref: 'refs/pull/42/merge',
  142. commit: '',
  143. allowUnsafePrCheckout: false
  144. })
  145. ).toThrow()
  146. })
  147. it('refuses pull_request_target when repository points at the fork', () => {
  148. setContext('pull_request_target', forkPullRequestTargetPayload())
  149. expect(() =>
  150. assertSafePrCheckout({
  151. qualifiedRepository: FORK_QUALIFIED_REPO,
  152. ref: 'refs/heads/main',
  153. commit: '',
  154. allowUnsafePrCheckout: false
  155. })
  156. ).toThrow()
  157. })
  158. it('allows pull_request_target checkout of an unrelated third-party repo', () => {
  159. setContext('pull_request_target', forkPullRequestTargetPayload())
  160. expect(() =>
  161. assertSafePrCheckout({
  162. qualifiedRepository: 'some-other/unrelated',
  163. ref: 'refs/heads/main',
  164. commit: '',
  165. allowUnsafePrCheckout: false
  166. })
  167. ).not.toThrow()
  168. })
  169. it('refuses pull_request_target ignoring repository case differences', () => {
  170. setContext('pull_request_target', forkPullRequestTargetPayload())
  171. expect(() =>
  172. assertSafePrCheckout({
  173. qualifiedRepository: FORK_QUALIFIED_REPO.toUpperCase(),
  174. ref: '',
  175. commit: '',
  176. allowUnsafePrCheckout: false
  177. })
  178. ).toThrow()
  179. })
  180. it('refuses pull_request_target ignoring commit SHA case differences', () => {
  181. setContext('pull_request_target', forkPullRequestTargetPayload())
  182. expect(() =>
  183. assertSafePrCheckout({
  184. qualifiedRepository: BASE_QUALIFIED_REPO,
  185. ref: '',
  186. commit: PR_HEAD_SHA.toUpperCase(),
  187. allowUnsafePrCheckout: false
  188. })
  189. ).toThrow()
  190. })
  191. it('allows pull_request_target fork PR checkout when opted in', () => {
  192. setContext('pull_request_target', forkPullRequestTargetPayload())
  193. expect(() =>
  194. assertSafePrCheckout({
  195. qualifiedRepository: BASE_QUALIFIED_REPO,
  196. ref: 'refs/pull/42/merge',
  197. commit: '',
  198. allowUnsafePrCheckout: true
  199. })
  200. ).not.toThrow()
  201. })
  202. it('refuses workflow_run fork PR head_commit.id checkout', () => {
  203. setContext('workflow_run', forkWorkflowRunPayload())
  204. expect(() =>
  205. assertSafePrCheckout({
  206. qualifiedRepository: BASE_QUALIFIED_REPO,
  207. ref: '',
  208. commit: WORKFLOW_RUN_HEAD_COMMIT_SHA,
  209. allowUnsafePrCheckout: false
  210. })
  211. ).toThrow()
  212. })
  213. it('refuses workflow_run with pull_request_target underlying event', () => {
  214. const payload = forkWorkflowRunPayload() as {
  215. workflow_run: {event: string}
  216. }
  217. payload.workflow_run.event = 'pull_request_target'
  218. setContext('workflow_run', payload)
  219. expect(() =>
  220. assertSafePrCheckout({
  221. qualifiedRepository: BASE_QUALIFIED_REPO,
  222. ref: '',
  223. commit: WORKFLOW_RUN_HEAD_COMMIT_SHA,
  224. allowUnsafePrCheckout: false
  225. })
  226. ).toThrow()
  227. })
  228. it('allows workflow_run same-repo PR (head_repository.id matches base)', () => {
  229. const payload = forkWorkflowRunPayload() as {
  230. workflow_run: {head_repository: {id: number}}
  231. }
  232. payload.workflow_run.head_repository.id = BASE_REPO_ID
  233. setContext('workflow_run', payload)
  234. expect(() =>
  235. assertSafePrCheckout({
  236. qualifiedRepository: BASE_QUALIFIED_REPO,
  237. ref: '',
  238. commit: WORKFLOW_RUN_HEAD_COMMIT_SHA,
  239. allowUnsafePrCheckout: false
  240. })
  241. ).not.toThrow()
  242. })
  243. })