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
diff options
context:
space:
mode:
Diffstat (limited to 'src/unsafe-pr-checkout-helper.ts')
-rw-r--r--src/unsafe-pr-checkout-helper.ts88
1 files changed, 88 insertions, 0 deletions
diff --git a/src/unsafe-pr-checkout-helper.ts b/src/unsafe-pr-checkout-helper.ts
new file mode 100644
index 0000000..efc0ef6
--- /dev/null
+++ b/src/unsafe-pr-checkout-helper.ts
@@ -0,0 +1,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())
+ }
+}