diff options
| author | Aiqiao Yan <55104035+aiqiaoy@users.noreply.github.com> | 2026-07-07 16:34:51 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-07 16:34:51 -0400 |
| commit | dc961c09ed360318cdce3e86b74c9ff0f37ed869 (patch) | |
| tree | de7b2bc6f86cea43e86b08f4fa208efe3857870e /__test__ | |
| parent | 65c48109f791f32b21b183a75cea8fc23cf659e6 (diff) | |
backport `allow-unsafe-pr-checkout` to v4 (#2502)
* 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)
Diffstat (limited to '__test__')
| -rw-r--r-- | __test__/git-auth-helper.test.ts | 3 | ||||
| -rw-r--r-- | __test__/input-helper.test.ts | 1 | ||||
| -rw-r--r-- | __test__/unsafe-pr-checkout-helper.test.ts | 267 |
3 files changed, 270 insertions, 1 deletions
diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts index 9acba54..92a61fe 100644 --- a/__test__/git-auth-helper.test.ts +++ b/__test__/git-auth-helper.test.ts @@ -1113,7 +1113,8 @@ async function setup(testName: string): Promise<void> { sshUser: '', workflowOrganizationId: 123456, setSafeDirectory: true, - githubServerUrl: githubServerUrl + githubServerUrl: githubServerUrl, + allowUnsafePrCheckout: false } } diff --git a/__test__/input-helper.test.ts b/__test__/input-helper.test.ts index 9514cb4..2b7e918 100644 --- a/__test__/input-helper.test.ts +++ b/__test__/input-helper.test.ts @@ -91,6 +91,7 @@ describe('input-helper tests', () => { expect(settings.repositoryOwner).toBe('some-owner') expect(settings.repositoryPath).toBe(gitHubWorkspace) expect(settings.setSafeDirectory).toBe(true) + expect(settings.allowUnsafePrCheckout).toBe(false) }) it('qualifies ref', async () => { diff --git a/__test__/unsafe-pr-checkout-helper.test.ts b/__test__/unsafe-pr-checkout-helper.test.ts new file mode 100644 index 0000000..9efa246 --- /dev/null +++ b/__test__/unsafe-pr-checkout-helper.test.ts @@ -0,0 +1,267 @@ +import * as github from '@actions/github' +import {assertSafePrCheckout} from '../lib/unsafe-pr-checkout-helper' + +// Shallow clone original @actions/github context +const originalContext = {...github.context} +const originalEventName = github.context.eventName +const originalPayload = github.context.payload + +const BASE_REPO_ID = 100 +const FORK_REPO_ID = 200 +const PR_HEAD_SHA = '1111111111111111111111111111111111111111' +const PR_MERGE_SHA = '2222222222222222222222222222222222222222' +const SAFE_BASE_SHA = '3333333333333333333333333333333333333333' +const WORKFLOW_RUN_HEAD_COMMIT_SHA = '4444444444444444444444444444444444444444' +const BASE_QUALIFIED_REPO = 'some-owner/some-repo' +const FORK_QUALIFIED_REPO = 'another-repo/fork' + +function setContext(eventName: string, payload: object): void { + ;(github.context as {eventName: string}).eventName = eventName + ;(github.context as {payload: object}).payload = payload +} + +function forkPullRequestTargetPayload(): object { + return { + repository: {id: BASE_REPO_ID}, + pull_request: { + head: { + sha: PR_HEAD_SHA, + repo: {id: FORK_REPO_ID, full_name: FORK_QUALIFIED_REPO} + }, + merge_commit_sha: PR_MERGE_SHA + } + } +} + +function sameRepoPullRequestTargetPayload(): object { + return { + repository: {id: BASE_REPO_ID}, + pull_request: { + head: { + sha: PR_HEAD_SHA, + repo: {id: BASE_REPO_ID, full_name: BASE_QUALIFIED_REPO} + }, + merge_commit_sha: PR_MERGE_SHA + } + } +} + +function forkWorkflowRunPayload(): object { + return { + repository: {id: BASE_REPO_ID}, + workflow_run: { + event: 'pull_request', + head_commit: {id: WORKFLOW_RUN_HEAD_COMMIT_SHA}, + head_repository: {id: FORK_REPO_ID, full_name: FORK_QUALIFIED_REPO} + } + } +} + +describe('unsafe-pr-checkout-helper', () => { + beforeAll(() => { + jest.spyOn(github.context, 'repo', 'get').mockReturnValue({ + owner: 'some-owner', + repo: 'some-repo' + }) + }) + + afterEach(() => { + ;(github.context as {eventName: string}).eventName = originalEventName + ;(github.context as {payload: object}).payload = originalPayload + }) + + afterAll(() => { + ;(github.context as {eventName: string}).eventName = + originalContext.eventName + ;(github.context as {payload: object}).payload = originalContext.payload + jest.restoreAllMocks() + }) + + it('allows pull_request events untouched', () => { + setContext('pull_request', forkPullRequestTargetPayload()) + expect(() => + assertSafePrCheckout({ + qualifiedRepository: 'attacker/fork', + ref: 'refs/pull/1/merge', + commit: '', + allowUnsafePrCheckout: false + }) + ).not.toThrow() + }) + + it('allows pull_request_target default checkout (base branch)', () => { + setContext('pull_request_target', forkPullRequestTargetPayload()) + expect(() => + assertSafePrCheckout({ + qualifiedRepository: BASE_QUALIFIED_REPO, + ref: 'refs/heads/main', + commit: SAFE_BASE_SHA, + allowUnsafePrCheckout: false + }) + ).not.toThrow() + }) + + it('allows same-repo pull_request_target checkout of PR head', () => { + setContext('pull_request_target', sameRepoPullRequestTargetPayload()) + expect(() => + assertSafePrCheckout({ + qualifiedRepository: BASE_QUALIFIED_REPO, + ref: '', + commit: PR_HEAD_SHA, + allowUnsafePrCheckout: false + }) + ).not.toThrow() + }) + + it('refuses pull_request_target fork PR head SHA checkout', () => { + setContext('pull_request_target', forkPullRequestTargetPayload()) + expect(() => + assertSafePrCheckout({ + qualifiedRepository: BASE_QUALIFIED_REPO, + ref: '', + commit: PR_HEAD_SHA, + allowUnsafePrCheckout: false + }) + ).toThrow(/Refusing to check out fork pull request code/) + }) + + it('refuses pull_request_target fork PR merge_commit_sha checkout', () => { + setContext('pull_request_target', forkPullRequestTargetPayload()) + expect(() => + assertSafePrCheckout({ + qualifiedRepository: BASE_QUALIFIED_REPO, + ref: '', + commit: PR_MERGE_SHA, + allowUnsafePrCheckout: false + }) + ).toThrow(/allow-unsafe-pr-checkout/) + }) + + it('refuses pull_request_target fork PR ref pattern (head)', () => { + setContext('pull_request_target', forkPullRequestTargetPayload()) + expect(() => + assertSafePrCheckout({ + qualifiedRepository: BASE_QUALIFIED_REPO, + ref: 'refs/pull/42/head', + commit: '', + allowUnsafePrCheckout: false + }) + ).toThrow() + }) + + it('refuses pull_request_target fork PR ref pattern (merge)', () => { + setContext('pull_request_target', forkPullRequestTargetPayload()) + expect(() => + assertSafePrCheckout({ + qualifiedRepository: BASE_QUALIFIED_REPO, + ref: 'refs/pull/42/merge', + commit: '', + allowUnsafePrCheckout: false + }) + ).toThrow() + }) + + it('refuses pull_request_target when repository points at the fork', () => { + setContext('pull_request_target', forkPullRequestTargetPayload()) + expect(() => + assertSafePrCheckout({ + qualifiedRepository: FORK_QUALIFIED_REPO, + ref: 'refs/heads/main', + commit: '', + allowUnsafePrCheckout: false + }) + ).toThrow() + }) + + it('allows pull_request_target checkout of an unrelated third-party repo', () => { + setContext('pull_request_target', forkPullRequestTargetPayload()) + expect(() => + assertSafePrCheckout({ + qualifiedRepository: 'some-other/unrelated', + ref: 'refs/heads/main', + commit: '', + allowUnsafePrCheckout: false + }) + ).not.toThrow() + }) + + it('refuses pull_request_target ignoring repository case differences', () => { + setContext('pull_request_target', forkPullRequestTargetPayload()) + expect(() => + assertSafePrCheckout({ + qualifiedRepository: FORK_QUALIFIED_REPO.toUpperCase(), + ref: '', + commit: '', + allowUnsafePrCheckout: false + }) + ).toThrow() + }) + + it('refuses pull_request_target ignoring commit SHA case differences', () => { + setContext('pull_request_target', forkPullRequestTargetPayload()) + expect(() => + assertSafePrCheckout({ + qualifiedRepository: BASE_QUALIFIED_REPO, + ref: '', + commit: PR_HEAD_SHA.toUpperCase(), + allowUnsafePrCheckout: false + }) + ).toThrow() + }) + + it('allows pull_request_target fork PR checkout when opted in', () => { + setContext('pull_request_target', forkPullRequestTargetPayload()) + expect(() => + assertSafePrCheckout({ + qualifiedRepository: BASE_QUALIFIED_REPO, + ref: 'refs/pull/42/merge', + commit: '', + allowUnsafePrCheckout: true + }) + ).not.toThrow() + }) + + it('refuses workflow_run fork PR head_commit.id checkout', () => { + setContext('workflow_run', forkWorkflowRunPayload()) + expect(() => + assertSafePrCheckout({ + qualifiedRepository: BASE_QUALIFIED_REPO, + ref: '', + commit: WORKFLOW_RUN_HEAD_COMMIT_SHA, + allowUnsafePrCheckout: false + }) + ).toThrow() + }) + + it('refuses workflow_run with pull_request_target underlying event', () => { + const payload = forkWorkflowRunPayload() as { + workflow_run: {event: string} + } + payload.workflow_run.event = 'pull_request_target' + setContext('workflow_run', payload) + expect(() => + assertSafePrCheckout({ + qualifiedRepository: BASE_QUALIFIED_REPO, + ref: '', + commit: WORKFLOW_RUN_HEAD_COMMIT_SHA, + allowUnsafePrCheckout: false + }) + ).toThrow() + }) + + it('allows workflow_run same-repo PR (head_repository.id matches base)', () => { + const payload = forkWorkflowRunPayload() as { + workflow_run: {head_repository: {id: number}} + } + payload.workflow_run.head_repository.id = BASE_REPO_ID + setContext('workflow_run', payload) + expect(() => + assertSafePrCheckout({ + qualifiedRepository: BASE_QUALIFIED_REPO, + ref: '', + commit: WORKFLOW_RUN_HEAD_COMMIT_SHA, + allowUnsafePrCheckout: false + }) + ).not.toThrow() + }) +}) |
