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:
authorAiqiao Yan <55104035+aiqiaoy@users.noreply.github.com>2026-07-07 16:34:16 -0400
committerGitHub <noreply@github.com>2026-07-07 16:34:16 -0400
commitced100813c147e2fc3ca692ddc52876ed2358c51 (patch)
treea3e9785ead5e277b5d38735b9436d8a90b9956ab /src
parentbc8e7a2fc551aab4af101037f06bd94d72c7df97 (diff)
backport `allow-unsafe-pr-checkout` to v2 (#2504)
* block checking out fork pr for pull_request_target and workflow_run (#2454) * block checking out fork pr for some events * address copilot and reviewer feedback * run prettier formatting * build * update urls * update readme * update description and url again * edit url one more time * update error wording (#2467) * bump upload-artifact * rebuild
Diffstat (limited to 'src')
-rw-r--r--src/git-source-settings.ts6
-rw-r--r--src/input-helper.ts15
-rw-r--r--src/ref-helper.ts2
-rw-r--r--src/unsafe-pr-checkout-helper.ts88
4 files changed, 110 insertions, 1 deletions
diff --git a/src/git-source-settings.ts b/src/git-source-settings.ts
index 6fa3960..82eb983 100644
--- a/src/git-source-settings.ts
+++ b/src/git-source-settings.ts
@@ -83,4 +83,10 @@ export interface IGitSourceSettings {
* Indicates whether to add repositoryPath as safe.directory in git global config
*/
setSafeDirectory: boolean
+
+ /**
+ * 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 8c2f901..1a090c7 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'
@@ -125,5 +126,19 @@ export async function getInputs(): Promise<IGitSourceSettings> {
// Set safe.directory in git global config.
result.setSafeDirectory =
(core.getInput('set-safe-directory') || 'true').toUpperCase() === 'TRUE'
+
+ // 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 209f49d..c9492f5 100644
--- a/src/ref-helper.ts
+++ b/src/ref-helper.ts
@@ -259,7 +259,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())
+ }
+}