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
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/git-source-settings.ts6
-rw-r--r--src/input-helper.ts14
-rw-r--r--src/ref-helper.ts2
-rw-r--r--src/unsafe-pr-checkout-helper.ts88
4 files changed, 109 insertions, 1 deletions
diff --git a/src/git-source-settings.ts b/src/git-source-settings.ts
index 4e41ac3..79041c4 100644
--- a/src/git-source-settings.ts
+++ b/src/git-source-settings.ts
@@ -118,4 +118,10 @@ export interface IGitSourceSettings {
* User override on the GitHub Server/Host URL that hosts the repository to be cloned
*/
githubServerUrl: string | undefined
+
+ /**
+ * Opt-in to allow checking out fork pull request code from a workflow
+ * triggered by pull_request_target or workflow_run.
+ */
+ allowUnsafePrCheckout: boolean
}
diff --git a/src/input-helper.ts b/src/input-helper.ts
index e0c61e2..2d20930 100644
--- a/src/input-helper.ts
+++ b/src/input-helper.ts
@@ -2,6 +2,7 @@ import * as core from '@actions/core'
import * as fsHelper from './fs-helper'
import * as github from '@actions/github'
import * as path from 'path'
+import * as unsafePrCheckoutHelper from './unsafe-pr-checkout-helper'
import * as workflowContextHelper from './workflow-context-helper'
import {IGitSourceSettings} from './git-source-settings'
@@ -161,5 +162,18 @@ export async function getInputs(): Promise<IGitSourceSettings> {
result.githubServerUrl = core.getInput('github-server-url')
core.debug(`GitHub Host URL = ${result.githubServerUrl}`)
+ // Allow unsafe PR checkout (opt-in for pull_request_target / workflow_run fork PRs)
+ result.allowUnsafePrCheckout =
+ (core.getInput('allow-unsafe-pr-checkout') || 'false').toUpperCase() ===
+ 'TRUE'
+ core.debug(`allow unsafe PR checkout = ${result.allowUnsafePrCheckout}`)
+
+ unsafePrCheckoutHelper.assertSafePrCheckout({
+ qualifiedRepository,
+ ref: result.ref,
+ commit: result.commit,
+ allowUnsafePrCheckout: result.allowUnsafePrCheckout
+ })
+
return result
}
diff --git a/src/ref-helper.ts b/src/ref-helper.ts
index 71e8b22..8751712 100644
--- a/src/ref-helper.ts
+++ b/src/ref-helper.ts
@@ -292,7 +292,7 @@ export async function checkCommitInfo(
}
}
-function fromPayload(path: string): any {
+export function fromPayload(path: string): any {
return select(github.context.payload, path)
}
diff --git a/src/unsafe-pr-checkout-helper.ts b/src/unsafe-pr-checkout-helper.ts
new file mode 100644
index 0000000..f6bf389
--- /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, review the risks ` +
+ `at https://gh.io/securely-using-pull_request_target and 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())
+ }
+}