Read-only mirror — the public repositories of the Forgejo instance, refreshed every 10 minutes. The live instance (fg.dieschoenewolke.de) is reachable only over the VPN; clone here with https://git.dieschoenewolke.de/git/<owner>/<repo>.git.
summaryrefslogtreecommitdiff
path: root/src/unsafe-pr-checkout-helper.ts
blob: efc0ef6210296bbf02aef8ae6037ec1848783974 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import * as github from '@actions/github'
import {fromPayload} from './ref-helper'

const PR_REF_PATTERN = /^refs\/pull\/[0-9]+\/(?:head|merge)$/

export interface IUnsafePrCheckoutInput {
  qualifiedRepository: string
  ref: string
  commit: string | undefined
  allowUnsafePrCheckout: boolean
}

export function assertSafePrCheckout(input: IUnsafePrCheckoutInput): void {
  if (input.allowUnsafePrCheckout) {
    return
  }

  const eventName = github.context.eventName
  if (eventName !== 'pull_request_target' && eventName !== 'workflow_run') {
    return
  }

  const baseRepoId = fromPayload('repository.id')
  if (typeof baseRepoId !== 'number') {
    return
  }

  let prHeadRepoId: unknown
  let prHeadRepoFullName: unknown
  const prShas: string[] = []

  if (eventName === 'pull_request_target') {
    prHeadRepoId = fromPayload('pull_request.head.repo.id')
    prHeadRepoFullName = fromPayload('pull_request.head.repo.full_name')
    pushIfSha(prShas, fromPayload('pull_request.head.sha'))
    pushIfSha(prShas, fromPayload('pull_request.merge_commit_sha'))
  } else {
    const wrEvent = fromPayload('workflow_run.event')
    if (typeof wrEvent !== 'string' || !wrEvent.startsWith('pull_request')) {
      return
    }
    prHeadRepoId = fromPayload('workflow_run.head_repository.id')
    prHeadRepoFullName = fromPayload('workflow_run.head_repository.full_name')
    pushIfSha(prShas, fromPayload('workflow_run.head_commit.id'))
    // For `pull_request_target`-triggered workflow_run, `head_sha` is the base
    // default branch SHA (not the PR head)
    if (wrEvent !== 'pull_request_target') {
      pushIfSha(prShas, fromPayload('workflow_run.head_sha'))
    }
  }

  // (A) Fork PR?
  if (typeof prHeadRepoId !== 'number' || prHeadRepoId === baseRepoId) {
    return
  }

  // (B) We cannot check for all fork PR refs so check to see
  // if the resolved input points to the fork PR sha we have in the payload
  const repositoryMatchesPrHead =
    typeof prHeadRepoFullName === 'string' &&
    input.qualifiedRepository.toLowerCase() === prHeadRepoFullName.toLowerCase()
  const refMatchesPullPattern = PR_REF_PATTERN.test(input.ref)
  const commitMatchesPrHeadSha =
    !!input.commit && prShas.includes(input.commit.toLowerCase())

  if (
    !repositoryMatchesPrHead &&
    !refMatchesPullPattern &&
    !commitMatchesPrHeadSha
  ) {
    return
  }

  throw new Error(
    `Refusing to check out fork pull request code from a '${eventName}' workflow. ` +
      `This workflow runs with the base repository's GITHUB_TOKEN, secrets, default-branch ` +
      `cache scope, and runner access. Fetching and executing a fork's code in that trusted ` +
      `context commonly leads to "pwn request" vulnerabilities. To opt in after reviewing ` +
      `the risks at https://gh.io/securely-using-pull_request_target, set ` +
      `'allow-unsafe-pr-checkout: true' on the actions/checkout step.`
  )
}

function pushIfSha(target: string[], value: unknown): void {
  if (typeof value === 'string' && value.length > 0) {
    target.push(value.toLowerCase())
  }
}