mirror of
https://github.com/actions/checkout.git
synced 2026-06-13 07:29:39 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be385d8fff |
@@ -1,9 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v6.0.3
|
|
||||||
* Fix checkout init for SHA-256 repositories by @yaananth in https://github.com/actions/checkout/pull/2439
|
|
||||||
* fix: expand merge commit SHA regex and add SHA-256 test cases by @yaananth in https://github.com/actions/checkout/pull/2414
|
|
||||||
|
|
||||||
## v6.0.2
|
## v6.0.2
|
||||||
* Fix tag handling: preserve annotations and explicit fetch-tags by @ericsciple in https://github.com/actions/checkout/pull/2356
|
* Fix tag handling: preserve annotations and explicit fetch-tags by @ericsciple in https://github.com/actions/checkout/pull/2356
|
||||||
|
|
||||||
|
|||||||
@@ -160,12 +160,6 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
# running from unless specified. Example URLs are https://github.com or
|
# running from unless specified. Example URLs are https://github.com or
|
||||||
# https://my-ghes-server.example.com
|
# https://my-ghes-server.example.com
|
||||||
github-server-url: ''
|
github-server-url: ''
|
||||||
|
|
||||||
# Required to check out fork pull request code from a workflow triggered by
|
|
||||||
# `pull_request_target` or `workflow_run`. See [Pwn Requests](todo:need-link) for
|
|
||||||
# the risks. Set to `true` only after reviewing the risks.
|
|
||||||
# Default: false
|
|
||||||
allow-unsafe-pr-checkout: ''
|
|
||||||
```
|
```
|
||||||
<!-- end usage -->
|
<!-- end usage -->
|
||||||
|
|
||||||
|
|||||||
@@ -974,6 +974,46 @@ describe('git-auth-helper tests', () => {
|
|||||||
).toBe(false)
|
).toBe(false)
|
||||||
expect((authHelper as any).testCredentialsConfigPath('')).toBe(false)
|
expect((authHelper as any).testCredentialsConfigPath('')).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const includeIfCleanupRegex_matchesBothVariants =
|
||||||
|
'includeIf cleanup regex matches both gitdir: and gitdir/i: keys'
|
||||||
|
it(includeIfCleanupRegex_matchesBothVariants, async () => {
|
||||||
|
// The cleanup regex must match both variants so credential
|
||||||
|
// removal works regardless of which was written
|
||||||
|
const regex = /^includeIf\.gitdir(\/i)?:/
|
||||||
|
expect(regex.test('includeIf.gitdir:D:/workspaces/repo/.git.path')).toBe(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(regex.test('includeIf.gitdir/i:D:/Workspaces/repo/.git.path')).toBe(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(regex.test('includeIf.gitdir/i:/github/workspace/.git.path')).toBe(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(regex.test('includeIf.gitdir:~/projects/foo/.git.path')).toBe(true)
|
||||||
|
expect(regex.test('includeIf.onbranch:main.path')).toBe(false)
|
||||||
|
expect(regex.test('include.path')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
const includeIfDirective_usesCorrectVariantForPlatform =
|
||||||
|
'includeIf directive uses gitdir/i on Windows and gitdir on other platforms'
|
||||||
|
it(includeIfDirective_usesCorrectVariantForPlatform, async () => {
|
||||||
|
await setup(includeIfDirective_usesCorrectVariantForPlatform)
|
||||||
|
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||||
|
await authHelper.configureAuth()
|
||||||
|
|
||||||
|
const localConfigContent = (
|
||||||
|
await fs.promises.readFile(localGitConfigPath)
|
||||||
|
).toString()
|
||||||
|
|
||||||
|
if (isWindows) {
|
||||||
|
expect(localConfigContent).toContain('includeIf.gitdir/i:')
|
||||||
|
expect(localConfigContent).not.toContain('includeIf.gitdir:')
|
||||||
|
} else {
|
||||||
|
expect(localConfigContent).toContain('includeIf.gitdir:')
|
||||||
|
expect(localConfigContent).not.toContain('includeIf.gitdir/i:')
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
async function setup(testName: string): Promise<void> {
|
async function setup(testName: string): Promise<void> {
|
||||||
@@ -1173,8 +1213,7 @@ async function setup(testName: string): Promise<void> {
|
|||||||
sshUser: '',
|
sshUser: '',
|
||||||
workflowOrganizationId: 123456,
|
workflowOrganizationId: 123456,
|
||||||
setSafeDirectory: true,
|
setSafeDirectory: true,
|
||||||
githubServerUrl: githubServerUrl,
|
githubServerUrl: githubServerUrl
|
||||||
allowUnsafePrCheckout: false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -378,59 +378,6 @@ describe('Test fetchDepth and fetchTags options', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('repository initialization object format', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
|
|
||||||
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('initializes SHA-256 repositories with the matching object format', async () => {
|
|
||||||
mockExec.mockImplementation((path, args, options) => {
|
|
||||||
if (args.includes('version')) {
|
|
||||||
options.listeners.stdout(Buffer.from('git version 2.50.1'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
|
||||||
|
|
||||||
git = await commandManager.createCommandManager('test', false, false)
|
|
||||||
|
|
||||||
await git.init('sha256')
|
|
||||||
|
|
||||||
expect(mockExec).toHaveBeenCalledWith(
|
|
||||||
expect.any(String),
|
|
||||||
['init', '--object-format=sha256', 'test'],
|
|
||||||
expect.any(Object)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('initializes SHA-1 repositories with existing default arguments', async () => {
|
|
||||||
mockExec.mockImplementation((path, args, options) => {
|
|
||||||
if (args.includes('version')) {
|
|
||||||
options.listeners.stdout(Buffer.from('git version 2.50.1'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
|
||||||
|
|
||||||
git = await commandManager.createCommandManager('test', false, false)
|
|
||||||
|
|
||||||
await git.init('sha1')
|
|
||||||
|
|
||||||
expect(mockExec).toHaveBeenCalledWith(
|
|
||||||
expect.any(String),
|
|
||||||
['init', 'test'],
|
|
||||||
expect.any(Object)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('git user-agent with orchestration ID', () => {
|
describe('git user-agent with orchestration ID', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
|
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import * as github from '@actions/github'
|
|
||||||
import * as githubApiHelper from '../lib/github-api-helper'
|
|
||||||
|
|
||||||
describe('github-api-helper object format', () => {
|
|
||||||
let getOctokitSpy: jest.SpyInstance
|
|
||||||
let debugSpy: jest.SpyInstance
|
|
||||||
let request: jest.Mock
|
|
||||||
|
|
||||||
function mockHashAlgorithmApi(hashAlgorithm: string): void {
|
|
||||||
request = jest.fn(async () => ({
|
|
||||||
data: {
|
|
||||||
hash_algorithm: hashAlgorithm
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
getOctokitSpy = jest.spyOn(github, 'getOctokit').mockReturnValue({
|
|
||||||
request
|
|
||||||
} as any)
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
debugSpy = jest.spyOn(core, 'debug').mockImplementation(jest.fn())
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('detects SHA-256 from the repository hash algorithm endpoint', async () => {
|
|
||||||
mockHashAlgorithmApi('sha256')
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
|
||||||
).resolves.toEqual({format: 'sha256', succeeded: true})
|
|
||||||
|
|
||||||
expect(getOctokitSpy).toHaveBeenCalledWith(
|
|
||||||
'token',
|
|
||||||
expect.objectContaining({baseUrl: 'https://api.github.com'})
|
|
||||||
)
|
|
||||||
expect(request).toHaveBeenCalledWith(
|
|
||||||
'GET /repos/{owner}/{repo}/hash-algorithm',
|
|
||||||
{owner: 'owner', repo: 'repo'}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('detects SHA-1 from the repository hash algorithm endpoint', async () => {
|
|
||||||
mockHashAlgorithmApi('sha1')
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
|
||||||
).resolves.toEqual({format: 'sha1', succeeded: true})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('detects object format from an existing commit without API calls', async () => {
|
|
||||||
const commitSha =
|
|
||||||
'9422233ca7ee1b17f1e905d0e141faf0c401556c41cdc6acd71c6bd685da2e92'
|
|
||||||
getOctokitSpy = jest.spyOn(github, 'getOctokit')
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
githubApiHelper.tryGetRepositoryObjectFormat(
|
|
||||||
'token',
|
|
||||||
'owner',
|
|
||||||
'repo',
|
|
||||||
undefined,
|
|
||||||
commitSha
|
|
||||||
)
|
|
||||||
).resolves.toEqual({format: 'sha256', succeeded: true})
|
|
||||||
|
|
||||||
expect(getOctokitSpy).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns unsuccessful when the hash algorithm endpoint value is not recognized', async () => {
|
|
||||||
mockHashAlgorithmApi('unknown')
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
|
||||||
).resolves.toEqual({format: '', succeeded: false})
|
|
||||||
expect(debugSpy).toHaveBeenCalledWith(
|
|
||||||
'Unable to determine repository object format from hash-algorithm endpoint'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns unsuccessful when the hash algorithm API lookup fails', async () => {
|
|
||||||
request = jest.fn(async () => {
|
|
||||||
throw new Error('not found')
|
|
||||||
})
|
|
||||||
jest.spyOn(github, 'getOctokit').mockReturnValue({
|
|
||||||
request
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
|
||||||
).resolves.toEqual({format: '', succeeded: false})
|
|
||||||
expect(debugSpy).toHaveBeenCalledWith(
|
|
||||||
'Unable to determine repository object format from hash-algorithm endpoint: not found'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -91,7 +91,6 @@ describe('input-helper tests', () => {
|
|||||||
expect(settings.repositoryOwner).toBe('some-owner')
|
expect(settings.repositoryOwner).toBe('some-owner')
|
||||||
expect(settings.repositoryPath).toBe(gitHubWorkspace)
|
expect(settings.repositoryPath).toBe(gitHubWorkspace)
|
||||||
expect(settings.setSafeDirectory).toBe(true)
|
expect(settings.setSafeDirectory).toBe(true)
|
||||||
expect(settings.allowUnsafePrCheckout).toBe(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('qualifies ref', async () => {
|
it('qualifies ref', async () => {
|
||||||
|
|||||||
@@ -1,254 +0,0 @@
|
|||||||
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'
|
|
||||||
|
|
||||||
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}
|
|
||||||
},
|
|
||||||
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}
|
|
||||||
},
|
|
||||||
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}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: 'attacker/fork',
|
|
||||||
ref: 'refs/heads/main',
|
|
||||||
commit: '',
|
|
||||||
allowUnsafePrCheckout: false
|
|
||||||
})
|
|
||||||
).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('refuses pull_request_target ignoring repository case differences', () => {
|
|
||||||
setContext('pull_request_target', forkPullRequestTargetPayload())
|
|
||||||
expect(() =>
|
|
||||||
assertSafePrCheckout({
|
|
||||||
qualifiedRepository: 'SOME-OWNER/SOME-REPO',
|
|
||||||
ref: '',
|
|
||||||
commit: PR_HEAD_SHA,
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -98,12 +98,6 @@ inputs:
|
|||||||
github-server-url:
|
github-server-url:
|
||||||
description: The base URL for the GitHub instance that you are trying to clone from, will use environment defaults to fetch from the same instance that the workflow is running from unless specified. Example URLs are https://github.com or https://my-ghes-server.example.com
|
description: The base URL for the GitHub instance that you are trying to clone from, will use environment defaults to fetch from the same instance that the workflow is running from unless specified. Example URLs are https://github.com or https://my-ghes-server.example.com
|
||||||
required: false
|
required: false
|
||||||
allow-unsafe-pr-checkout:
|
|
||||||
description: >
|
|
||||||
Required to check out fork pull request code from a workflow triggered by
|
|
||||||
`pull_request_target` or `workflow_run`. See [Pwn Requests](todo:need-link)
|
|
||||||
for the risks. Set to `true` only after reviewing the risks.
|
|
||||||
default: false
|
|
||||||
outputs:
|
outputs:
|
||||||
ref:
|
ref:
|
||||||
description: 'The branch, tag or SHA that was checked out'
|
description: 'The branch, tag or SHA that was checked out'
|
||||||
|
|||||||
Vendored
+16
-163
@@ -151,6 +151,12 @@ const stateHelper = __importStar(__nccwpck_require__(4866));
|
|||||||
const urlHelper = __importStar(__nccwpck_require__(9437));
|
const urlHelper = __importStar(__nccwpck_require__(9437));
|
||||||
const uuid_1 = __nccwpck_require__(5840);
|
const uuid_1 = __nccwpck_require__(5840);
|
||||||
const IS_WINDOWS = process.platform === 'win32';
|
const IS_WINDOWS = process.platform === 'win32';
|
||||||
|
// Use case-insensitive gitdir matching on Windows to handle path casing mismatches
|
||||||
|
// between the runner's GITHUB_WORKSPACE and the actual filesystem casing.
|
||||||
|
// See: https://github.com/actions/checkout/issues/2345
|
||||||
|
const INCLUDE_IF_GITDIR = IS_WINDOWS
|
||||||
|
? 'includeIf.gitdir/i:'
|
||||||
|
: 'includeIf.gitdir:';
|
||||||
const SSH_COMMAND_KEY = 'core.sshCommand';
|
const SSH_COMMAND_KEY = 'core.sshCommand';
|
||||||
function createAuthHelper(git, settings) {
|
function createAuthHelper(git, settings) {
|
||||||
return new GitAuthHelper(git, settings);
|
return new GitAuthHelper(git, settings);
|
||||||
@@ -270,7 +276,7 @@ class GitAuthHelper {
|
|||||||
let submoduleGitDir = path.dirname(configPath); // The config file is at .git/modules/submodule-name/config
|
let submoduleGitDir = path.dirname(configPath); // The config file is at .git/modules/submodule-name/config
|
||||||
submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
|
submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
|
||||||
// Configure host includeIf
|
// Configure host includeIf
|
||||||
yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, // globalConfig?
|
yield this.git.config(`${INCLUDE_IF_GITDIR}${submoduleGitDir}.path`, credentialsConfigPath, false, // globalConfig?
|
||||||
false, // add?
|
false, // add?
|
||||||
configPath);
|
configPath);
|
||||||
// Container submodule git directory
|
// Container submodule git directory
|
||||||
@@ -280,7 +286,7 @@ class GitAuthHelper {
|
|||||||
relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
|
relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
|
||||||
const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleGitDir);
|
const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleGitDir);
|
||||||
// Configure container includeIf
|
// Configure container includeIf
|
||||||
yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, // globalConfig?
|
yield this.git.config(`${INCLUDE_IF_GITDIR}${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, // globalConfig?
|
||||||
false, // add?
|
false, // add?
|
||||||
configPath);
|
configPath);
|
||||||
}
|
}
|
||||||
@@ -410,10 +416,10 @@ class GitAuthHelper {
|
|||||||
let gitDir = path.join(this.git.getWorkingDirectory(), '.git');
|
let gitDir = path.join(this.git.getWorkingDirectory(), '.git');
|
||||||
gitDir = gitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
|
gitDir = gitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
|
||||||
// Configure host includeIf
|
// Configure host includeIf
|
||||||
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`;
|
const hostIncludeKey = `${INCLUDE_IF_GITDIR}${gitDir}.path`;
|
||||||
yield this.git.config(hostIncludeKey, credentialsConfigPath);
|
yield this.git.config(hostIncludeKey, credentialsConfigPath);
|
||||||
// Configure host includeIf for worktrees
|
// Configure host includeIf for worktrees
|
||||||
const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`;
|
const hostWorktreeIncludeKey = `${INCLUDE_IF_GITDIR}${gitDir}/worktrees/*.path`;
|
||||||
yield this.git.config(hostWorktreeIncludeKey, credentialsConfigPath);
|
yield this.git.config(hostWorktreeIncludeKey, credentialsConfigPath);
|
||||||
// Container git directory
|
// Container git directory
|
||||||
const workingDirectory = this.git.getWorkingDirectory();
|
const workingDirectory = this.git.getWorkingDirectory();
|
||||||
@@ -425,10 +431,10 @@ class GitAuthHelper {
|
|||||||
// Container credentials config path
|
// Container credentials config path
|
||||||
const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
|
const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
|
||||||
// Configure container includeIf
|
// Configure container includeIf
|
||||||
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`;
|
const containerIncludeKey = `${INCLUDE_IF_GITDIR}${containerGitDir}.path`;
|
||||||
yield this.git.config(containerIncludeKey, containerCredentialsPath);
|
yield this.git.config(containerIncludeKey, containerCredentialsPath);
|
||||||
// Configure container includeIf for worktrees
|
// Configure container includeIf for worktrees
|
||||||
const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`;
|
const containerWorktreeIncludeKey = `${INCLUDE_IF_GITDIR}${containerGitDir}/worktrees/*.path`;
|
||||||
yield this.git.config(containerWorktreeIncludeKey, containerCredentialsPath);
|
yield this.git.config(containerWorktreeIncludeKey, containerCredentialsPath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -565,7 +571,7 @@ class GitAuthHelper {
|
|||||||
const credentialsPaths = new Set();
|
const credentialsPaths = new Set();
|
||||||
try {
|
try {
|
||||||
// Get all includeIf.gitdir keys
|
// Get all includeIf.gitdir keys
|
||||||
const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, // globalConfig?
|
const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir(/i)?:', false, // globalConfig?
|
||||||
configPath);
|
configPath);
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
// Get all values for this key
|
// Get all values for this key
|
||||||
@@ -896,14 +902,9 @@ class GitCommandManager {
|
|||||||
getWorkingDirectory() {
|
getWorkingDirectory() {
|
||||||
return this.workingDirectory;
|
return this.workingDirectory;
|
||||||
}
|
}
|
||||||
init(objectFormat) {
|
init() {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
const args = ['init'];
|
yield this.execGit(['init', this.workingDirectory]);
|
||||||
if (objectFormat === 'sha256') {
|
|
||||||
args.push('--object-format=sha256');
|
|
||||||
}
|
|
||||||
args.push(this.workingDirectory);
|
|
||||||
yield this.execGit(args);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
isDetached() {
|
isDetached() {
|
||||||
@@ -1491,17 +1492,8 @@ function getSource(settings) {
|
|||||||
stateHelper.setRepositoryPath(settings.repositoryPath);
|
stateHelper.setRepositoryPath(settings.repositoryPath);
|
||||||
// Initialize the repository
|
// Initialize the repository
|
||||||
if (!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))) {
|
if (!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))) {
|
||||||
core.startGroup('Determining repository object format');
|
|
||||||
const objectFormatResult = yield githubApiHelper.tryGetRepositoryObjectFormat(settings.authToken, settings.repositoryOwner, settings.repositoryName, settings.githubServerUrl, settings.commit);
|
|
||||||
const objectFormat = objectFormatResult.succeeded
|
|
||||||
? objectFormatResult.format
|
|
||||||
: '';
|
|
||||||
if (objectFormat === 'sha256') {
|
|
||||||
core.info('Detected SHA-256 repository object format');
|
|
||||||
}
|
|
||||||
core.endGroup();
|
|
||||||
core.startGroup('Initializing the repository');
|
core.startGroup('Initializing the repository');
|
||||||
yield git.init(objectFormat);
|
yield git.init();
|
||||||
yield git.remoteAdd('origin', repositoryUrl);
|
yield git.remoteAdd('origin', repositoryUrl);
|
||||||
core.endGroup();
|
core.endGroup();
|
||||||
}
|
}
|
||||||
@@ -1824,7 +1816,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|||||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||||
exports.downloadRepository = downloadRepository;
|
exports.downloadRepository = downloadRepository;
|
||||||
exports.getDefaultBranch = getDefaultBranch;
|
exports.getDefaultBranch = getDefaultBranch;
|
||||||
exports.tryGetRepositoryObjectFormat = tryGetRepositoryObjectFormat;
|
|
||||||
const assert = __importStar(__nccwpck_require__(9491));
|
const assert = __importStar(__nccwpck_require__(9491));
|
||||||
const core = __importStar(__nccwpck_require__(2186));
|
const core = __importStar(__nccwpck_require__(2186));
|
||||||
const fs = __importStar(__nccwpck_require__(7147));
|
const fs = __importStar(__nccwpck_require__(7147));
|
||||||
@@ -1926,40 +1917,6 @@ function getDefaultBranch(authToken, owner, repo, baseUrl) {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function tryGetRepositoryObjectFormat(authToken, owner, repo, baseUrl, commit) {
|
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
|
||||||
var _a;
|
|
||||||
const commitFormat = getObjectFormat(commit);
|
|
||||||
if (commitFormat) {
|
|
||||||
return { format: commitFormat, succeeded: true };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const octokit = github.getOctokit(authToken, {
|
|
||||||
baseUrl: (0, url_helper_1.getServerApiUrl)(baseUrl)
|
|
||||||
});
|
|
||||||
const response = yield octokit.request('GET /repos/{owner}/{repo}/hash-algorithm', { owner, repo });
|
|
||||||
const hashAlgorithm = response.data.hash_algorithm;
|
|
||||||
if (hashAlgorithm === 'sha256' || hashAlgorithm === 'sha1') {
|
|
||||||
return { format: hashAlgorithm, succeeded: true };
|
|
||||||
}
|
|
||||||
core.debug('Unable to determine repository object format from hash-algorithm endpoint');
|
|
||||||
return { format: '', succeeded: false };
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
core.debug(`Unable to determine repository object format from hash-algorithm endpoint: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
||||||
return { format: '', succeeded: false };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function getObjectFormat(sha) {
|
|
||||||
if (/^[0-9a-fA-F]{64}$/.test(sha || '')) {
|
|
||||||
return 'sha256';
|
|
||||||
}
|
|
||||||
if (/^[0-9a-fA-F]{40}$/.test(sha || '')) {
|
|
||||||
return 'sha1';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
function downloadArchive(authToken, owner, repo, ref, commit, baseUrl) {
|
function downloadArchive(authToken, owner, repo, ref, commit, baseUrl) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
const octokit = github.getOctokit(authToken, {
|
const octokit = github.getOctokit(authToken, {
|
||||||
@@ -2023,7 +1980,6 @@ const core = __importStar(__nccwpck_require__(2186));
|
|||||||
const fsHelper = __importStar(__nccwpck_require__(7219));
|
const fsHelper = __importStar(__nccwpck_require__(7219));
|
||||||
const github = __importStar(__nccwpck_require__(5438));
|
const github = __importStar(__nccwpck_require__(5438));
|
||||||
const path = __importStar(__nccwpck_require__(1017));
|
const path = __importStar(__nccwpck_require__(1017));
|
||||||
const unsafePrCheckoutHelper = __importStar(__nccwpck_require__(843));
|
|
||||||
const workflowContextHelper = __importStar(__nccwpck_require__(9568));
|
const workflowContextHelper = __importStar(__nccwpck_require__(9568));
|
||||||
function getInputs() {
|
function getInputs() {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
@@ -2145,17 +2101,6 @@ function getInputs() {
|
|||||||
// Determine the GitHub URL that the repository is being hosted from
|
// Determine the GitHub URL that the repository is being hosted from
|
||||||
result.githubServerUrl = core.getInput('github-server-url');
|
result.githubServerUrl = core.getInput('github-server-url');
|
||||||
core.debug(`GitHub Host URL = ${result.githubServerUrl}`);
|
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;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2296,7 +2241,6 @@ exports.getRefSpecForAllHistory = getRefSpecForAllHistory;
|
|||||||
exports.getRefSpec = getRefSpec;
|
exports.getRefSpec = getRefSpec;
|
||||||
exports.testRef = testRef;
|
exports.testRef = testRef;
|
||||||
exports.checkCommitInfo = checkCommitInfo;
|
exports.checkCommitInfo = checkCommitInfo;
|
||||||
exports.fromPayload = fromPayload;
|
|
||||||
const core = __importStar(__nccwpck_require__(2186));
|
const core = __importStar(__nccwpck_require__(2186));
|
||||||
const github = __importStar(__nccwpck_require__(5438));
|
const github = __importStar(__nccwpck_require__(5438));
|
||||||
const url_helper_1 = __nccwpck_require__(9437);
|
const url_helper_1 = __nccwpck_require__(9437);
|
||||||
@@ -2745,97 +2689,6 @@ if (!exports.IsPost) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
|
||||||
|
|
||||||
/***/ 843:
|
|
||||||
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
||||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
||||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
||||||
}
|
|
||||||
Object.defineProperty(o, k2, desc);
|
|
||||||
}) : (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
o[k2] = m[k];
|
|
||||||
}));
|
|
||||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
||||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
||||||
}) : function(o, v) {
|
|
||||||
o["default"] = v;
|
|
||||||
});
|
|
||||||
var __importStar = (this && this.__importStar) || function (mod) {
|
|
||||||
if (mod && mod.__esModule) return mod;
|
|
||||||
var result = {};
|
|
||||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
||||||
__setModuleDefault(result, mod);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
||||||
exports.assertSafePrCheckout = assertSafePrCheckout;
|
|
||||||
const github = __importStar(__nccwpck_require__(5438));
|
|
||||||
const ref_helper_1 = __nccwpck_require__(8601);
|
|
||||||
const PR_REF_PATTERN = /^refs\/pull\/[0-9]+\/(?:head|merge)$/;
|
|
||||||
function assertSafePrCheckout(input) {
|
|
||||||
if (input.allowUnsafePrCheckout) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const eventName = github.context.eventName;
|
|
||||||
if (eventName !== 'pull_request_target' && eventName !== 'workflow_run') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const baseRepoId = (0, ref_helper_1.fromPayload)('repository.id');
|
|
||||||
if (typeof baseRepoId !== 'number') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let prHeadRepoId;
|
|
||||||
const prShas = [];
|
|
||||||
if (eventName === 'pull_request_target') {
|
|
||||||
prHeadRepoId = (0, ref_helper_1.fromPayload)('pull_request.head.repo.id');
|
|
||||||
pushIfSha(prShas, (0, ref_helper_1.fromPayload)('pull_request.head.sha'));
|
|
||||||
pushIfSha(prShas, (0, ref_helper_1.fromPayload)('pull_request.merge_commit_sha'));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const wrEvent = (0, ref_helper_1.fromPayload)('workflow_run.event');
|
|
||||||
if (typeof wrEvent !== 'string' || !wrEvent.startsWith('pull_request')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
prHeadRepoId = (0, ref_helper_1.fromPayload)('workflow_run.head_repository.id');
|
|
||||||
pushIfSha(prShas, (0, ref_helper_1.fromPayload)('workflow_run.head_commit.id'));
|
|
||||||
}
|
|
||||||
// (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 baseQualifiedRepository = `${github.context.repo.owner}/${github.context.repo.repo}`;
|
|
||||||
const repositoryDiffersFromBase = input.qualifiedRepository.toLowerCase() !==
|
|
||||||
baseQualifiedRepository.toLowerCase();
|
|
||||||
const refMatchesPullPattern = PR_REF_PATTERN.test(input.ref);
|
|
||||||
const commitMatchesPrHeadSha = !!input.commit && prShas.includes(input.commit.toLowerCase());
|
|
||||||
if (!repositoryDiffersFromBase &&
|
|
||||||
!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 fork's code in that trusted context is a ` +
|
|
||||||
`"pwn request" supply-chain attack pattern. To opt in after reviewing the risk, set ` +
|
|
||||||
`'allow-unsafe-pr-checkout: true' on the actions/checkout step.`);
|
|
||||||
}
|
|
||||||
function pushIfSha(target, value) {
|
|
||||||
if (typeof value === 'string' && value.length > 0) {
|
|
||||||
target.push(value.toLowerCase());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
/***/ 9437:
|
/***/ 9437:
|
||||||
|
|||||||
+13
-7
@@ -13,6 +13,12 @@ import {IGitCommandManager} from './git-command-manager'
|
|||||||
import {IGitSourceSettings} from './git-source-settings'
|
import {IGitSourceSettings} from './git-source-settings'
|
||||||
|
|
||||||
const IS_WINDOWS = process.platform === 'win32'
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
// Use case-insensitive gitdir matching on Windows to handle path casing mismatches
|
||||||
|
// between the runner's GITHUB_WORKSPACE and the actual filesystem casing.
|
||||||
|
// See: https://github.com/actions/checkout/issues/2345
|
||||||
|
const INCLUDE_IF_GITDIR = IS_WINDOWS
|
||||||
|
? 'includeIf.gitdir/i:'
|
||||||
|
: 'includeIf.gitdir:'
|
||||||
const SSH_COMMAND_KEY = 'core.sshCommand'
|
const SSH_COMMAND_KEY = 'core.sshCommand'
|
||||||
|
|
||||||
export interface IGitAuthHelper {
|
export interface IGitAuthHelper {
|
||||||
@@ -182,7 +188,7 @@ class GitAuthHelper {
|
|||||||
|
|
||||||
// Configure host includeIf
|
// Configure host includeIf
|
||||||
await this.git.config(
|
await this.git.config(
|
||||||
`includeIf.gitdir:${submoduleGitDir}.path`,
|
`${INCLUDE_IF_GITDIR}${submoduleGitDir}.path`,
|
||||||
credentialsConfigPath,
|
credentialsConfigPath,
|
||||||
false, // globalConfig?
|
false, // globalConfig?
|
||||||
false, // add?
|
false, // add?
|
||||||
@@ -204,7 +210,7 @@ class GitAuthHelper {
|
|||||||
|
|
||||||
// Configure container includeIf
|
// Configure container includeIf
|
||||||
await this.git.config(
|
await this.git.config(
|
||||||
`includeIf.gitdir:${containerSubmoduleGitDir}.path`,
|
`${INCLUDE_IF_GITDIR}${containerSubmoduleGitDir}.path`,
|
||||||
containerCredentialsPath,
|
containerCredentialsPath,
|
||||||
false, // globalConfig?
|
false, // globalConfig?
|
||||||
false, // add?
|
false, // add?
|
||||||
@@ -371,11 +377,11 @@ class GitAuthHelper {
|
|||||||
gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
|
||||||
|
|
||||||
// Configure host includeIf
|
// Configure host includeIf
|
||||||
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`
|
const hostIncludeKey = `${INCLUDE_IF_GITDIR}${gitDir}.path`
|
||||||
await this.git.config(hostIncludeKey, credentialsConfigPath)
|
await this.git.config(hostIncludeKey, credentialsConfigPath)
|
||||||
|
|
||||||
// Configure host includeIf for worktrees
|
// Configure host includeIf for worktrees
|
||||||
const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`
|
const hostWorktreeIncludeKey = `${INCLUDE_IF_GITDIR}${gitDir}/worktrees/*.path`
|
||||||
await this.git.config(hostWorktreeIncludeKey, credentialsConfigPath)
|
await this.git.config(hostWorktreeIncludeKey, credentialsConfigPath)
|
||||||
|
|
||||||
// Container git directory
|
// Container git directory
|
||||||
@@ -397,11 +403,11 @@ class GitAuthHelper {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Configure container includeIf
|
// Configure container includeIf
|
||||||
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`
|
const containerIncludeKey = `${INCLUDE_IF_GITDIR}${containerGitDir}.path`
|
||||||
await this.git.config(containerIncludeKey, containerCredentialsPath)
|
await this.git.config(containerIncludeKey, containerCredentialsPath)
|
||||||
|
|
||||||
// Configure container includeIf for worktrees
|
// Configure container includeIf for worktrees
|
||||||
const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`
|
const containerWorktreeIncludeKey = `${INCLUDE_IF_GITDIR}${containerGitDir}/worktrees/*.path`
|
||||||
await this.git.config(
|
await this.git.config(
|
||||||
containerWorktreeIncludeKey,
|
containerWorktreeIncludeKey,
|
||||||
containerCredentialsPath
|
containerCredentialsPath
|
||||||
@@ -554,7 +560,7 @@ class GitAuthHelper {
|
|||||||
try {
|
try {
|
||||||
// Get all includeIf.gitdir keys
|
// Get all includeIf.gitdir keys
|
||||||
const keys = await this.git.tryGetConfigKeys(
|
const keys = await this.git.tryGetConfigKeys(
|
||||||
'^includeIf\\.gitdir:',
|
'^includeIf\\.gitdir(/i)?:',
|
||||||
false, // globalConfig?
|
false, // globalConfig?
|
||||||
configPath
|
configPath
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export interface IGitCommandManager {
|
|||||||
getDefaultBranch(repositoryUrl: string): Promise<string>
|
getDefaultBranch(repositoryUrl: string): Promise<string>
|
||||||
getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
|
getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
|
||||||
getWorkingDirectory(): string
|
getWorkingDirectory(): string
|
||||||
init(objectFormat?: string): Promise<void>
|
init(): Promise<void>
|
||||||
isDetached(): Promise<boolean>
|
isDetached(): Promise<boolean>
|
||||||
lfsFetch(ref: string): Promise<void>
|
lfsFetch(ref: string): Promise<void>
|
||||||
lfsInstall(): Promise<void>
|
lfsInstall(): Promise<void>
|
||||||
@@ -364,14 +364,8 @@ class GitCommandManager {
|
|||||||
return this.workingDirectory
|
return this.workingDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(objectFormat?: string): Promise<void> {
|
async init(): Promise<void> {
|
||||||
const args = ['init']
|
await this.execGit(['init', this.workingDirectory])
|
||||||
if (objectFormat === 'sha256') {
|
|
||||||
args.push('--object-format=sha256')
|
|
||||||
}
|
|
||||||
args.push(this.workingDirectory)
|
|
||||||
|
|
||||||
await this.execGit(args)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async isDetached(): Promise<boolean> {
|
async isDetached(): Promise<boolean> {
|
||||||
|
|||||||
@@ -109,25 +109,8 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
|||||||
if (
|
if (
|
||||||
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
||||||
) {
|
) {
|
||||||
core.startGroup('Determining repository object format')
|
|
||||||
const objectFormatResult =
|
|
||||||
await githubApiHelper.tryGetRepositoryObjectFormat(
|
|
||||||
settings.authToken,
|
|
||||||
settings.repositoryOwner,
|
|
||||||
settings.repositoryName,
|
|
||||||
settings.githubServerUrl,
|
|
||||||
settings.commit
|
|
||||||
)
|
|
||||||
const objectFormat = objectFormatResult.succeeded
|
|
||||||
? objectFormatResult.format
|
|
||||||
: ''
|
|
||||||
if (objectFormat === 'sha256') {
|
|
||||||
core.info('Detected SHA-256 repository object format')
|
|
||||||
}
|
|
||||||
core.endGroup()
|
|
||||||
|
|
||||||
core.startGroup('Initializing the repository')
|
core.startGroup('Initializing the repository')
|
||||||
await git.init(objectFormat)
|
await git.init()
|
||||||
await git.remoteAdd('origin', repositoryUrl)
|
await git.remoteAdd('origin', repositoryUrl)
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,10 +118,4 @@ export interface IGitSourceSettings {
|
|||||||
* User override on the GitHub Server/Host URL that hosts the repository to be cloned
|
* User override on the GitHub Server/Host URL that hosts the repository to be cloned
|
||||||
*/
|
*/
|
||||||
githubServerUrl: string | undefined
|
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,6 @@ import {getServerApiUrl} from './url-helper'
|
|||||||
|
|
||||||
const IS_WINDOWS = process.platform === 'win32'
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
|
||||||
export interface RepositoryObjectFormatResult {
|
|
||||||
format: string
|
|
||||||
succeeded: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function downloadRepository(
|
export async function downloadRepository(
|
||||||
authToken: string,
|
authToken: string,
|
||||||
owner: string,
|
owner: string,
|
||||||
@@ -127,53 +122,6 @@ export async function getDefaultBranch(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function tryGetRepositoryObjectFormat(
|
|
||||||
authToken: string,
|
|
||||||
owner: string,
|
|
||||||
repo: string,
|
|
||||||
baseUrl?: string,
|
|
||||||
commit?: string
|
|
||||||
): Promise<RepositoryObjectFormatResult> {
|
|
||||||
const commitFormat = getObjectFormat(commit)
|
|
||||||
if (commitFormat) {
|
|
||||||
return {format: commitFormat, succeeded: true}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const octokit = github.getOctokit(authToken, {
|
|
||||||
baseUrl: getServerApiUrl(baseUrl)
|
|
||||||
})
|
|
||||||
const response = await octokit.request(
|
|
||||||
'GET /repos/{owner}/{repo}/hash-algorithm',
|
|
||||||
{owner, repo}
|
|
||||||
)
|
|
||||||
const hashAlgorithm = response.data.hash_algorithm
|
|
||||||
if (hashAlgorithm === 'sha256' || hashAlgorithm === 'sha1') {
|
|
||||||
return {format: hashAlgorithm, succeeded: true}
|
|
||||||
}
|
|
||||||
|
|
||||||
core.debug(
|
|
||||||
'Unable to determine repository object format from hash-algorithm endpoint'
|
|
||||||
)
|
|
||||||
return {format: '', succeeded: false}
|
|
||||||
} catch (err) {
|
|
||||||
core.debug(
|
|
||||||
`Unable to determine repository object format from hash-algorithm endpoint: ${(err as any)?.message ?? err}`
|
|
||||||
)
|
|
||||||
return {format: '', succeeded: false}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getObjectFormat(sha?: string): string {
|
|
||||||
if (/^[0-9a-fA-F]{64}$/.test(sha || '')) {
|
|
||||||
return 'sha256'
|
|
||||||
}
|
|
||||||
if (/^[0-9a-fA-F]{40}$/.test(sha || '')) {
|
|
||||||
return 'sha1'
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadArchive(
|
async function downloadArchive(
|
||||||
authToken: string,
|
authToken: string,
|
||||||
owner: string,
|
owner: string,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import * as core from '@actions/core'
|
|||||||
import * as fsHelper from './fs-helper'
|
import * as fsHelper from './fs-helper'
|
||||||
import * as github from '@actions/github'
|
import * as github from '@actions/github'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as unsafePrCheckoutHelper from './unsafe-pr-checkout-helper'
|
|
||||||
import * as workflowContextHelper from './workflow-context-helper'
|
import * as workflowContextHelper from './workflow-context-helper'
|
||||||
import {IGitSourceSettings} from './git-source-settings'
|
import {IGitSourceSettings} from './git-source-settings'
|
||||||
|
|
||||||
@@ -162,18 +161,5 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
|||||||
result.githubServerUrl = core.getInput('github-server-url')
|
result.githubServerUrl = core.getInput('github-server-url')
|
||||||
core.debug(`GitHub Host URL = ${result.githubServerUrl}`)
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -292,7 +292,7 @@ export async function checkCommitInfo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fromPayload(path: string): any {
|
function fromPayload(path: string): any {
|
||||||
return select(github.context.payload, path)
|
return select(github.context.payload, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
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
|
|
||||||
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
|
|
||||||
const prShas: string[] = []
|
|
||||||
|
|
||||||
if (eventName === 'pull_request_target') {
|
|
||||||
prHeadRepoId = fromPayload('pull_request.head.repo.id')
|
|
||||||
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')
|
|
||||||
pushIfSha(prShas, fromPayload('workflow_run.head_commit.id'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// (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 baseQualifiedRepository = `${github.context.repo.owner}/${github.context.repo.repo}`
|
|
||||||
const repositoryDiffersFromBase =
|
|
||||||
input.qualifiedRepository.toLowerCase() !==
|
|
||||||
baseQualifiedRepository.toLowerCase()
|
|
||||||
const refMatchesPullPattern = PR_REF_PATTERN.test(input.ref)
|
|
||||||
const commitMatchesPrHeadSha =
|
|
||||||
!!input.commit && prShas.includes(input.commit.toLowerCase())
|
|
||||||
|
|
||||||
if (
|
|
||||||
!repositoryDiffersFromBase &&
|
|
||||||
!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 fork's code in that trusted context is a ` +
|
|
||||||
`"pwn request" supply-chain attack pattern. To opt in after reviewing the risk, 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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user