mirror of
https://github.com/actions/checkout.git
synced 2026-08-01 23:14:23 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0717577d45 | ||
|
|
262cdb5f1c | ||
|
|
ee0669bd1c | ||
|
|
dc323e67f1 | ||
|
|
e2f20e631a | ||
|
|
b2eb13baee | ||
|
|
7884fcad6b | ||
|
|
f67ee5d622 | ||
|
|
f25a3a9f25 |
2
.github/workflows/check-dist.yml
vendored
2
.github/workflows/check-dist.yml
vendored
@ -44,7 +44,7 @@ jobs:
|
||||
fi
|
||||
|
||||
# If dist/ was different than expected, upload the expected version as an artifact
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
|
||||
with:
|
||||
name: dist
|
||||
|
||||
40
.github/workflows/test.yml
vendored
40
.github/workflows/test.yml
vendored
@ -142,7 +142,7 @@ jobs:
|
||||
options: --dns 127.0.0.1
|
||||
services:
|
||||
squid-proxy:
|
||||
image: datadog/squid:latest
|
||||
image: ubuntu/squid:latest
|
||||
ports:
|
||||
- 3128:3128
|
||||
env:
|
||||
@ -205,3 +205,41 @@ jobs:
|
||||
path: basic
|
||||
- name: Verify basic
|
||||
run: __test__/verify-basic.sh --archive
|
||||
|
||||
test-git-container:
|
||||
runs-on: ubuntu-latest
|
||||
container: bitnami/git:latest
|
||||
steps:
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: v3
|
||||
|
||||
# Basic checkout using git
|
||||
- name: Checkout basic
|
||||
uses: ./v3
|
||||
with:
|
||||
ref: test-data/v2/basic
|
||||
- name: Verify basic
|
||||
run: |
|
||||
if [ ! -f "./basic-file.txt" ]; then
|
||||
echo "Expected basic file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify .git folder
|
||||
if [ ! -d "./.git" ]; then
|
||||
echo "Expected ./.git folder to exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify auth token
|
||||
git config --global --add safe.directory "*"
|
||||
git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
|
||||
|
||||
# needed to make checkout post cleanup succeed
|
||||
- name: Fix Checkout v3
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: v3
|
||||
BIN
.licenses/npm/@actions/core.dep.yml
generated
BIN
.licenses/npm/@actions/core.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/@actions/http-client-2.0.1.dep.yml
generated
Normal file
BIN
.licenses/npm/@actions/http-client-2.0.1.dep.yml
generated
Normal file
Binary file not shown.
BIN
.licenses/npm/@actions/io.dep.yml
generated
BIN
.licenses/npm/@actions/io.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/node-fetch.dep.yml
generated
BIN
.licenses/npm/node-fetch.dep.yml
generated
Binary file not shown.
BIN
.licenses/npm/qs.dep.yml
generated
BIN
.licenses/npm/qs.dep.yml
generated
Binary file not shown.
Binary file not shown.
BIN
.licenses/npm/uuid-8.3.2.dep.yml
generated
Normal file
BIN
.licenses/npm/uuid-8.3.2.dep.yml
generated
Normal file
Binary file not shown.
@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## v2.5.0
|
||||
- [Bump @actions/core to v1.10.0](https://github.com/actions/checkout/pull/962)
|
||||
|
||||
## v2.4.2
|
||||
- [Add input `set-safe-directory`](https://github.com/actions/checkout/pull/776)
|
||||
|
||||
## v2.4.1
|
||||
- [Set the safe directory option on git to prevent git commands failing when running in containers](https://github.com/actions/checkout/pull/762)
|
||||
|
||||
## v2.3.1
|
||||
|
||||
- [Fix default branch resolution for .wiki and when using SSH](https://github.com/actions/checkout/pull/284)
|
||||
|
||||
14
README.md
14
README.md
@ -105,6 +105,20 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
|
||||
#
|
||||
# Default: false
|
||||
submodules: ''
|
||||
|
||||
# Add repository path as safe.directory for Git global config by running `git
|
||||
# config --global --add safe.directory <path>`
|
||||
# Default: true
|
||||
set-safe-directory: ''
|
||||
|
||||
# Required to check out fork pull request code from a workflow triggered by
|
||||
# `pull_request_target` or `workflow_run`. These workflows run 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. Set to `true` only after reviewing the
|
||||
# risks at https://gh.io/securely-using-pull_request_target.
|
||||
# Default: false
|
||||
allow-unsafe-pr-checkout: ''
|
||||
```
|
||||
<!-- end usage -->
|
||||
|
||||
|
||||
@ -643,10 +643,11 @@ describe('git-auth-helper tests', () => {
|
||||
expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
|
||||
})
|
||||
|
||||
const removeGlobalAuth_removesOverride = 'removeGlobalAuth removes override'
|
||||
it(removeGlobalAuth_removesOverride, async () => {
|
||||
const removeGlobalConfig_removesOverride =
|
||||
'removeGlobalConfig removes override'
|
||||
it(removeGlobalConfig_removesOverride, async () => {
|
||||
// Arrange
|
||||
await setup(removeGlobalAuth_removesOverride)
|
||||
await setup(removeGlobalConfig_removesOverride)
|
||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
await authHelper.configureAuth()
|
||||
await authHelper.configureGlobalAuth()
|
||||
@ -655,7 +656,7 @@ describe('git-auth-helper tests', () => {
|
||||
await fs.promises.stat(path.join(git.env['HOME'], '.gitconfig'))
|
||||
|
||||
// Act
|
||||
await authHelper.removeGlobalAuth()
|
||||
await authHelper.removeGlobalConfig()
|
||||
|
||||
// Assert
|
||||
expect(git.env['HOME']).toBeUndefined()
|
||||
@ -776,7 +777,9 @@ async function setup(testName: string): Promise<void> {
|
||||
sshKey: sshPath ? 'some ssh private key' : '',
|
||||
sshKnownHosts: '',
|
||||
sshStrict: true,
|
||||
workflowOrganizationId: 123456
|
||||
workflowOrganizationId: 123456,
|
||||
setSafeDirectory: true,
|
||||
allowUnsafePrCheckout: false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,15 +12,24 @@ const gitHubWorkspace = path.resolve('/checkout-tests/workspace')
|
||||
// Inputs for mock @actions/core
|
||||
let inputs = {} as any
|
||||
|
||||
// Replicate @actions/core getInput behavior: it trims whitespace by default
|
||||
// (String.prototype.trim(), which strips characters such as a leading U+FEFF BOM)
|
||||
// unless trimWhitespace is explicitly set to false.
|
||||
const getInputImpl = (name: string, options?: {trimWhitespace?: boolean}) => {
|
||||
const val = inputs[name] ?? ''
|
||||
if (options && options.trimWhitespace === false) {
|
||||
return val
|
||||
}
|
||||
return typeof val === 'string' ? val.trim() : val
|
||||
}
|
||||
|
||||
// Shallow clone original @actions/github context
|
||||
let originalContext = {...github.context}
|
||||
|
||||
describe('input-helper tests', () => {
|
||||
beforeAll(() => {
|
||||
// Mock getInput
|
||||
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
|
||||
return inputs[name]
|
||||
})
|
||||
jest.spyOn(core, 'getInput').mockImplementation(getInputImpl as any)
|
||||
|
||||
// Mock error/warning/info/debug
|
||||
jest.spyOn(core, 'error').mockImplementation(jest.fn())
|
||||
@ -85,6 +94,8 @@ describe('input-helper tests', () => {
|
||||
expect(settings.repositoryName).toBe('some-repo')
|
||||
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 () => {
|
||||
@ -134,8 +145,76 @@ describe('input-helper tests', () => {
|
||||
expect(settings.commit).toBeFalsy()
|
||||
})
|
||||
|
||||
it('does not reclassify a ref as sha when a BOM is prefixed', async () => {
|
||||
// A fork branch named "<U+FEFF>" + 40 hex chars. core.getInput trims the
|
||||
// BOM by default, which previously collapsed this into a bare SHA and
|
||||
// bypassed the unsafe fork PR checkout guard.
|
||||
inputs.ref = '\uFEFF522d932fae5296da51fdf431934425ecf891c6a2'
|
||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||
expect(settings.commit).toBeFalsy()
|
||||
expect(settings.ref).toBe('522d932fae5296da51fdf431934425ecf891c6a2')
|
||||
})
|
||||
|
||||
it('treats a sha surrounded by ascii whitespace as a commit', async () => {
|
||||
// ASCII whitespace can only come from the workflow author's YAML (git ref
|
||||
// names cannot contain it), so trimming it and treating the value as a
|
||||
// commit is safe.
|
||||
inputs.ref = ' 1111111111222222222233333333334444444444 '
|
||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||
expect(settings.ref).toBeFalsy()
|
||||
expect(settings.commit).toBe('1111111111222222222233333333334444444444')
|
||||
})
|
||||
|
||||
it('sets workflow organization ID', async () => {
|
||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||
expect(settings.workflowOrganizationId).toBe(123456)
|
||||
})
|
||||
|
||||
describe('unsafe PR checkout guard', () => {
|
||||
const forkPayload = {
|
||||
repository: {id: 100},
|
||||
pull_request: {
|
||||
head: {
|
||||
sha: '1234567890123456789012345678901234567890',
|
||||
repo: {id: 200, full_name: 'attacker/fork'}
|
||||
},
|
||||
merge_commit_sha: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
||||
}
|
||||
}
|
||||
|
||||
it('allows the default self-checkout on a fork pull_request_target', async () => {
|
||||
const originalEvent = github.context.eventName
|
||||
const originalPayload = github.context.payload
|
||||
const originalSha = github.context.sha
|
||||
try {
|
||||
github.context.eventName = 'pull_request_target'
|
||||
github.context.payload = forkPayload as any
|
||||
// Simulate a rebase/fast-forward merge where the base tip (event SHA)
|
||||
// equals the PR head SHA. The default self-checkout must still succeed.
|
||||
github.context.sha = '1234567890123456789012345678901234567890'
|
||||
const settings: IGitSourceSettings = await inputHelper.getInputs()
|
||||
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
|
||||
} finally {
|
||||
github.context.eventName = originalEvent
|
||||
github.context.payload = originalPayload
|
||||
github.context.sha = originalSha
|
||||
}
|
||||
})
|
||||
|
||||
it('refuses an explicit fork repository on pull_request_target', async () => {
|
||||
const originalEvent = github.context.eventName
|
||||
const originalPayload = github.context.payload
|
||||
try {
|
||||
github.context.eventName = 'pull_request_target'
|
||||
github.context.payload = forkPayload as any
|
||||
inputs.repository = 'attacker/fork'
|
||||
await expect(inputHelper.getInputs()).rejects.toThrow(
|
||||
/Refusing to check out fork pull request code/
|
||||
)
|
||||
} finally {
|
||||
github.context.eventName = originalEvent
|
||||
github.context.payload = originalPayload
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
267
__test__/unsafe-pr-checkout-helper.test.ts
Normal file
267
__test__/unsafe-pr-checkout-helper.test.ts
Normal file
@ -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()
|
||||
})
|
||||
})
|
||||
12
action.yml
12
action.yml
@ -68,6 +68,18 @@ inputs:
|
||||
When the `ssh-key` input is not provided, SSH URLs beginning with `git@github.com:` are
|
||||
converted to HTTPS.
|
||||
default: false
|
||||
set-safe-directory:
|
||||
description: Add repository path as safe.directory for Git global config by running `git config --global --add safe.directory <path>`
|
||||
default: true
|
||||
allow-unsafe-pr-checkout:
|
||||
description: >
|
||||
Required to check out fork pull request code from a workflow triggered by
|
||||
`pull_request_target` or `workflow_run`. These workflows run 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. Set to `true`
|
||||
only after reviewing the risks at https://gh.io/securely-using-pull_request_target.
|
||||
default: false
|
||||
runs:
|
||||
using: node12
|
||||
main: dist/index.js
|
||||
|
||||
3433
dist/index.js
vendored
3433
dist/index.js
vendored
File diff suppressed because it is too large
Load Diff
4179
package-lock.json
generated
4179
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "checkout",
|
||||
"version": "2.0.2",
|
||||
"version": "2.6.0",
|
||||
"description": "checkout action",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
@ -28,10 +28,10 @@
|
||||
},
|
||||
"homepage": "https://github.com/actions/checkout#readme",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.6",
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/exec": "^1.0.1",
|
||||
"@actions/github": "^2.2.0",
|
||||
"@actions/io": "^1.0.1",
|
||||
"@actions/io": "^1.1.2",
|
||||
"@actions/tool-cache": "^1.1.2",
|
||||
"uuid": "^3.3.3"
|
||||
},
|
||||
@ -39,11 +39,12 @@
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/node": "^12.7.12",
|
||||
"@types/uuid": "^3.4.6",
|
||||
"@typescript-eslint/parser": "^5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"@zeit/ncc": "^0.20.5",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-github": "^4.3.2",
|
||||
"eslint-plugin-jest": "^25.2.2",
|
||||
"eslint-plugin-jest": "^25.7.0",
|
||||
"jest": "^27.3.0",
|
||||
"jest-circus": "^27.3.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
|
||||
@ -19,8 +19,9 @@ export interface IGitAuthHelper {
|
||||
configureAuth(): Promise<void>
|
||||
configureGlobalAuth(): Promise<void>
|
||||
configureSubmoduleAuth(): Promise<void>
|
||||
configureTempGlobalConfig(): Promise<string>
|
||||
removeAuth(): Promise<void>
|
||||
removeGlobalAuth(): Promise<void>
|
||||
removeGlobalConfig(): Promise<void>
|
||||
}
|
||||
|
||||
export function createAuthHelper(
|
||||
@ -80,7 +81,11 @@ class GitAuthHelper {
|
||||
await this.configureToken()
|
||||
}
|
||||
|
||||
async configureGlobalAuth(): Promise<void> {
|
||||
async configureTempGlobalConfig(): Promise<string> {
|
||||
// Already setup global config
|
||||
if (this.temporaryHomePath?.length > 0) {
|
||||
return path.join(this.temporaryHomePath, '.gitconfig')
|
||||
}
|
||||
// Create a temp home directory
|
||||
const runnerTemp = process.env['RUNNER_TEMP'] || ''
|
||||
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
|
||||
@ -110,13 +115,19 @@ class GitAuthHelper {
|
||||
await fs.promises.writeFile(newGitConfigPath, '')
|
||||
}
|
||||
|
||||
try {
|
||||
// Override HOME
|
||||
core.info(
|
||||
`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`
|
||||
)
|
||||
this.git.setEnvironmentVariable('HOME', this.temporaryHomePath)
|
||||
// Override HOME
|
||||
core.info(
|
||||
`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`
|
||||
)
|
||||
this.git.setEnvironmentVariable('HOME', this.temporaryHomePath)
|
||||
|
||||
return newGitConfigPath
|
||||
}
|
||||
|
||||
async configureGlobalAuth(): Promise<void> {
|
||||
// 'configureTempGlobalConfig' noops if already set, just returns the path
|
||||
const newGitConfigPath = await this.configureTempGlobalConfig()
|
||||
try {
|
||||
// Configure the token
|
||||
await this.configureToken(newGitConfigPath, true)
|
||||
|
||||
@ -146,7 +157,8 @@ class GitAuthHelper {
|
||||
// by process creation audit events, which are commonly logged. For more information,
|
||||
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
||||
const output = await this.git.submoduleForeach(
|
||||
`git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url`,
|
||||
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
|
||||
`sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
|
||||
this.settings.nestedSubmodules
|
||||
)
|
||||
|
||||
@ -181,10 +193,12 @@ class GitAuthHelper {
|
||||
await this.removeToken()
|
||||
}
|
||||
|
||||
async removeGlobalAuth(): Promise<void> {
|
||||
core.debug(`Unsetting HOME override`)
|
||||
this.git.removeEnvironmentVariable('HOME')
|
||||
await io.rmRF(this.temporaryHomePath)
|
||||
async removeGlobalConfig(): Promise<void> {
|
||||
if (this.temporaryHomePath?.length > 0) {
|
||||
core.debug(`Unsetting HOME override`)
|
||||
this.git.removeEnvironmentVariable('HOME')
|
||||
await io.rmRF(this.temporaryHomePath)
|
||||
}
|
||||
}
|
||||
|
||||
private async configureSsh(): Promise<void> {
|
||||
@ -233,7 +247,7 @@ class GitAuthHelper {
|
||||
if (this.settings.sshKnownHosts) {
|
||||
knownHosts += `# Begin from input known hosts\n${this.settings.sshKnownHosts}\n# end from input known hosts\n`
|
||||
}
|
||||
knownHosts += `# Begin implicitly added github.com\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n# End implicitly added github.com\n`
|
||||
knownHosts += `# Begin implicitly added github.com\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=\n# End implicitly added github.com\n`
|
||||
this.sshKnownHostsPath = path.join(runnerTemp, `${uniqueId}_known_hosts`)
|
||||
stateHelper.setSshKnownHostsPath(this.sshKnownHostsPath)
|
||||
await fs.promises.writeFile(this.sshKnownHostsPath, knownHosts)
|
||||
@ -352,7 +366,8 @@ class GitAuthHelper {
|
||||
|
||||
const pattern = regexpHelper.escape(configKey)
|
||||
await this.git.submoduleForeach(
|
||||
`git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :`,
|
||||
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
|
||||
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
@ -36,68 +36,94 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
const git = await getGitCommandManager(settings)
|
||||
core.endGroup()
|
||||
|
||||
// Prepare existing directory, otherwise recreate
|
||||
if (isExisting) {
|
||||
await gitDirectoryHelper.prepareExistingDirectory(
|
||||
git,
|
||||
settings.repositoryPath,
|
||||
repositoryUrl,
|
||||
settings.clean,
|
||||
settings.ref
|
||||
)
|
||||
}
|
||||
let authHelper: gitAuthHelper.IGitAuthHelper | null = null
|
||||
try {
|
||||
if (git) {
|
||||
authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
if (settings.setSafeDirectory) {
|
||||
// Setup the repository path as a safe directory, so if we pass this into a container job with a different user it doesn't fail
|
||||
// Otherwise all git commands we run in a container fail
|
||||
await authHelper.configureTempGlobalConfig()
|
||||
core.info(
|
||||
`Adding repository directory to the temporary git global config as a safe directory`
|
||||
)
|
||||
|
||||
if (!git) {
|
||||
// Downloading using REST API
|
||||
core.info(`The repository will be downloaded using the GitHub REST API`)
|
||||
core.info(
|
||||
`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`
|
||||
)
|
||||
if (settings.submodules) {
|
||||
throw new Error(
|
||||
`Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
|
||||
)
|
||||
} else if (settings.sshKey) {
|
||||
throw new Error(
|
||||
`Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
|
||||
await git
|
||||
.config('safe.directory', settings.repositoryPath, true, true)
|
||||
.catch(error => {
|
||||
core.info(
|
||||
`Failed to initialize safe directory with error: ${error}`
|
||||
)
|
||||
})
|
||||
|
||||
stateHelper.setSafeDirectory()
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare existing directory, otherwise recreate
|
||||
if (isExisting) {
|
||||
await gitDirectoryHelper.prepareExistingDirectory(
|
||||
git,
|
||||
settings.repositoryPath,
|
||||
repositoryUrl,
|
||||
settings.clean,
|
||||
settings.ref
|
||||
)
|
||||
}
|
||||
|
||||
await githubApiHelper.downloadRepository(
|
||||
settings.authToken,
|
||||
settings.repositoryOwner,
|
||||
settings.repositoryName,
|
||||
settings.ref,
|
||||
settings.commit,
|
||||
settings.repositoryPath
|
||||
)
|
||||
return
|
||||
}
|
||||
if (!git) {
|
||||
// Downloading using REST API
|
||||
core.info(`The repository will be downloaded using the GitHub REST API`)
|
||||
core.info(
|
||||
`To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`
|
||||
)
|
||||
if (settings.submodules) {
|
||||
throw new Error(
|
||||
`Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
|
||||
)
|
||||
} else if (settings.sshKey) {
|
||||
throw new Error(
|
||||
`Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
|
||||
)
|
||||
}
|
||||
|
||||
// Save state for POST action
|
||||
stateHelper.setRepositoryPath(settings.repositoryPath)
|
||||
await githubApiHelper.downloadRepository(
|
||||
settings.authToken,
|
||||
settings.repositoryOwner,
|
||||
settings.repositoryName,
|
||||
settings.ref,
|
||||
settings.commit,
|
||||
settings.repositoryPath
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize the repository
|
||||
if (
|
||||
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
||||
) {
|
||||
core.startGroup('Initializing the repository')
|
||||
await git.init()
|
||||
await git.remoteAdd('origin', repositoryUrl)
|
||||
// Save state for POST action
|
||||
stateHelper.setRepositoryPath(settings.repositoryPath)
|
||||
|
||||
// Initialize the repository
|
||||
if (
|
||||
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
||||
) {
|
||||
core.startGroup('Initializing the repository')
|
||||
await git.init()
|
||||
await git.remoteAdd('origin', repositoryUrl)
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
// Disable automatic garbage collection
|
||||
core.startGroup('Disabling automatic garbage collection')
|
||||
if (!(await git.tryDisableAutomaticGarbageCollection())) {
|
||||
core.warning(
|
||||
`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
|
||||
)
|
||||
}
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
// Disable automatic garbage collection
|
||||
core.startGroup('Disabling automatic garbage collection')
|
||||
if (!(await git.tryDisableAutomaticGarbageCollection())) {
|
||||
core.warning(
|
||||
`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
|
||||
)
|
||||
}
|
||||
core.endGroup()
|
||||
|
||||
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
try {
|
||||
// If we didn't initialize it above, do it now
|
||||
if (!authHelper) {
|
||||
authHelper = gitAuthHelper.createAuthHelper(git, settings)
|
||||
}
|
||||
// Configure auth
|
||||
core.startGroup('Setting up auth')
|
||||
await authHelper.configureAuth()
|
||||
@ -170,34 +196,26 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
|
||||
// Submodules
|
||||
if (settings.submodules) {
|
||||
try {
|
||||
// Temporarily override global config
|
||||
core.startGroup('Setting up auth for fetching submodules')
|
||||
await authHelper.configureGlobalAuth()
|
||||
core.endGroup()
|
||||
// Temporarily override global config
|
||||
core.startGroup('Setting up auth for fetching submodules')
|
||||
await authHelper.configureGlobalAuth()
|
||||
core.endGroup()
|
||||
|
||||
// Checkout submodules
|
||||
core.startGroup('Fetching submodules')
|
||||
await git.submoduleSync(settings.nestedSubmodules)
|
||||
await git.submoduleUpdate(
|
||||
settings.fetchDepth,
|
||||
settings.nestedSubmodules
|
||||
)
|
||||
await git.submoduleForeach(
|
||||
'git config --local gc.auto 0',
|
||||
settings.nestedSubmodules
|
||||
)
|
||||
core.endGroup()
|
||||
// Checkout submodules
|
||||
core.startGroup('Fetching submodules')
|
||||
await git.submoduleSync(settings.nestedSubmodules)
|
||||
await git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules)
|
||||
await git.submoduleForeach(
|
||||
'git config --local gc.auto 0',
|
||||
settings.nestedSubmodules
|
||||
)
|
||||
core.endGroup()
|
||||
|
||||
// Persist credentials
|
||||
if (settings.persistCredentials) {
|
||||
core.startGroup('Persisting credentials for submodules')
|
||||
await authHelper.configureSubmoduleAuth()
|
||||
core.endGroup()
|
||||
}
|
||||
} finally {
|
||||
// Remove temporary global config override
|
||||
await authHelper.removeGlobalAuth()
|
||||
// Persist credentials
|
||||
if (settings.persistCredentials) {
|
||||
core.startGroup('Persisting credentials for submodules')
|
||||
await authHelper.configureSubmoduleAuth()
|
||||
core.endGroup()
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,10 +236,13 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||
)
|
||||
} finally {
|
||||
// Remove auth
|
||||
if (!settings.persistCredentials) {
|
||||
core.startGroup('Removing auth')
|
||||
await authHelper.removeAuth()
|
||||
core.endGroup()
|
||||
if (authHelper) {
|
||||
if (!settings.persistCredentials) {
|
||||
core.startGroup('Removing auth')
|
||||
await authHelper.removeAuth()
|
||||
core.endGroup()
|
||||
}
|
||||
authHelper.removeGlobalConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -244,7 +265,26 @@ export async function cleanup(repositoryPath: string): Promise<void> {
|
||||
|
||||
// Remove auth
|
||||
const authHelper = gitAuthHelper.createAuthHelper(git)
|
||||
await authHelper.removeAuth()
|
||||
try {
|
||||
if (stateHelper.PostSetSafeDirectory) {
|
||||
// Setup the repository path as a safe directory, so if we pass this into a container job with a different user it doesn't fail
|
||||
// Otherwise all git commands we run in a container fail
|
||||
await authHelper.configureTempGlobalConfig()
|
||||
core.info(
|
||||
`Adding repository directory to the temporary git global config as a safe directory`
|
||||
)
|
||||
|
||||
await git
|
||||
.config('safe.directory', repositoryPath, true, true)
|
||||
.catch(error => {
|
||||
core.info(`Failed to initialize safe directory with error: ${error}`)
|
||||
})
|
||||
}
|
||||
|
||||
await authHelper.removeAuth()
|
||||
} finally {
|
||||
await authHelper.removeGlobalConfig()
|
||||
}
|
||||
}
|
||||
|
||||
async function getGitCommandManager(
|
||||
|
||||
@ -78,4 +78,15 @@ export interface IGitSourceSettings {
|
||||
* Organization ID for the currently running workflow (used for auth settings)
|
||||
*/
|
||||
workflowOrganizationId: number | undefined
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -58,6 +59,23 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
||||
|
||||
// Source branch, source version
|
||||
result.ref = core.getInput('ref')
|
||||
// core.getInput()'s default trim strips a range of Unicode characters such as a
|
||||
// leading BOM (U+FEFF) or NBSP (U+00A0). Those are valid in a git ref name, so
|
||||
// a fork branch named "<BOM>" + 40 hex chars would trim down to a bare SHA and
|
||||
// be silently reclassified as a commit, bypassing the unsafe fork PR checkout
|
||||
// guard.
|
||||
//
|
||||
// The trim below strips only the ASCII whitespace characters which are all forbidden
|
||||
// in a git branch name.
|
||||
// \t U+0009 horizontal tab - ASCII control, forbidden in ref names
|
||||
// \n U+000A line feed - ASCII control, forbidden in ref names
|
||||
// \v U+000B vertical tab - ASCII control, forbidden in ref names
|
||||
// \f U+000C form feed - ASCII control, forbidden in ref names
|
||||
// \r U+000D carriage return - ASCII control, forbidden in ref names
|
||||
// ' ' U+0020 space - forbidden in ref names
|
||||
const asciiTrimmedRef = core
|
||||
.getInput('ref', {trimWhitespace: false})
|
||||
.replace(/^[\t\n\v\f\r ]+|[\t\n\v\f\r ]+$/g, '')
|
||||
if (!result.ref) {
|
||||
if (isWorkflowRepository) {
|
||||
result.ref = github.context.ref
|
||||
@ -71,8 +89,8 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
||||
}
|
||||
}
|
||||
// SHA?
|
||||
else if (result.ref.match(/^[0-9a-fA-F]{40}$/)) {
|
||||
result.commit = result.ref
|
||||
else if (asciiTrimmedRef.match(/^[0-9a-fA-F]{40}$/)) {
|
||||
result.commit = asciiTrimmedRef
|
||||
result.ref = ''
|
||||
}
|
||||
core.debug(`ref = '${result.ref}'`)
|
||||
@ -122,5 +140,29 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
||||
// Workflow organization ID
|
||||
result.workflowOrganizationId = await workflowContextHelper.getOrganizationId()
|
||||
|
||||
// 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}`)
|
||||
|
||||
// The default self-checkout (this repository with no explicit ref) always
|
||||
// resolves to the trusted ref/commit GitHub set for the triggering event, so
|
||||
// the fork-checkout guard only needs to run when the caller customized the
|
||||
// repository or ref.
|
||||
const isDefaultCheckout = isWorkflowRepository && !core.getInput('ref')
|
||||
if (!isDefaultCheckout) {
|
||||
unsafePrCheckoutHelper.assertSafePrCheckout({
|
||||
qualifiedRepository,
|
||||
ref: result.ref,
|
||||
commit: result.commit,
|
||||
allowUnsafePrCheckout: result.allowUnsafePrCheckout
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@ -5,4 +5,4 @@ set -e
|
||||
src/misc/licensed-download.sh
|
||||
|
||||
echo 'Running: licensed cached'
|
||||
_temp/licensed-3.3.1/licensed status
|
||||
_temp/licensed-3.6.0/licensed status
|
||||
@ -2,23 +2,23 @@
|
||||
|
||||
set -e
|
||||
|
||||
if [ ! -f _temp/licensed-3.3.1.done ]; then
|
||||
if [ ! -f _temp/licensed-3.6.0.done ]; then
|
||||
echo 'Clearing temp'
|
||||
rm -rf _temp/licensed-3.3.1 || true
|
||||
rm -rf _temp/licensed-3.6.0 || true
|
||||
|
||||
echo 'Downloading licensed'
|
||||
mkdir -p _temp/licensed-3.3.1
|
||||
pushd _temp/licensed-3.3.1
|
||||
mkdir -p _temp/licensed-3.6.0
|
||||
pushd _temp/licensed-3.6.0
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
curl -Lfs -o licensed.tar.gz https://github.com/github/licensed/releases/download/3.3.1/licensed-3.3.1-darwin-x64.tar.gz
|
||||
curl -Lfs -o licensed.tar.gz https://github.com/github/licensed/releases/download/3.6.0/licensed-3.6.0-darwin-x64.tar.gz
|
||||
else
|
||||
curl -Lfs -o licensed.tar.gz https://github.com/github/licensed/releases/download/3.3.1/licensed-3.3.1-linux-x64.tar.gz
|
||||
curl -Lfs -o licensed.tar.gz https://github.com/github/licensed/releases/download/3.6.0/licensed-3.6.0-linux-x64.tar.gz
|
||||
fi
|
||||
|
||||
echo 'Extracting licenesed'
|
||||
tar -xzf licensed.tar.gz
|
||||
popd
|
||||
touch _temp/licensed-3.3.1.done
|
||||
touch _temp/licensed-3.6.0.done
|
||||
else
|
||||
echo 'Licensed already downloaded'
|
||||
fi
|
||||
|
||||
@ -5,4 +5,4 @@ set -e
|
||||
src/misc/licensed-download.sh
|
||||
|
||||
echo 'Running: licensed cached'
|
||||
_temp/licensed-3.3.1/licensed cache
|
||||
_temp/licensed-3.6.0/licensed cache
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -1,58 +1,60 @@
|
||||
import * as coreCommand from '@actions/core/lib/command'
|
||||
import * as core from '@actions/core'
|
||||
|
||||
/**
|
||||
* Indicates whether the POST action is running
|
||||
*/
|
||||
export const IsPost = !!process.env['STATE_isPost']
|
||||
export const IsPost = !!core.getState('isPost')
|
||||
|
||||
/**
|
||||
* The repository path for the POST action. The value is empty during the MAIN action.
|
||||
*/
|
||||
export const RepositoryPath =
|
||||
(process.env['STATE_repositoryPath'] as string) || ''
|
||||
export const RepositoryPath = core.getState('repositoryPath')
|
||||
|
||||
/**
|
||||
* The set-safe-directory for the POST action. The value is set if input: 'safe-directory' is set during the MAIN action.
|
||||
*/
|
||||
export const PostSetSafeDirectory = core.getState('setSafeDirectory') === 'true'
|
||||
|
||||
/**
|
||||
* The SSH key path for the POST action. The value is empty during the MAIN action.
|
||||
*/
|
||||
export const SshKeyPath = (process.env['STATE_sshKeyPath'] as string) || ''
|
||||
export const SshKeyPath = core.getState('sshKeyPath')
|
||||
|
||||
/**
|
||||
* The SSH known hosts path for the POST action. The value is empty during the MAIN action.
|
||||
*/
|
||||
export const SshKnownHostsPath =
|
||||
(process.env['STATE_sshKnownHostsPath'] as string) || ''
|
||||
export const SshKnownHostsPath = core.getState('sshKnownHostsPath')
|
||||
|
||||
/**
|
||||
* Save the repository path so the POST action can retrieve the value.
|
||||
*/
|
||||
export function setRepositoryPath(repositoryPath: string) {
|
||||
coreCommand.issueCommand(
|
||||
'save-state',
|
||||
{name: 'repositoryPath'},
|
||||
repositoryPath
|
||||
)
|
||||
core.saveState('repositoryPath', repositoryPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the SSH key path so the POST action can retrieve the value.
|
||||
*/
|
||||
export function setSshKeyPath(sshKeyPath: string) {
|
||||
coreCommand.issueCommand('save-state', {name: 'sshKeyPath'}, sshKeyPath)
|
||||
core.saveState('sshKeyPath', sshKeyPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the SSH known hosts path so the POST action can retrieve the value.
|
||||
*/
|
||||
export function setSshKnownHostsPath(sshKnownHostsPath: string) {
|
||||
coreCommand.issueCommand(
|
||||
'save-state',
|
||||
{name: 'sshKnownHostsPath'},
|
||||
sshKnownHostsPath
|
||||
)
|
||||
core.saveState('sshKnownHostsPath', sshKnownHostsPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the sef-safe-directory input so the POST action can retrieve the value.
|
||||
*/
|
||||
export function setSafeDirectory() {
|
||||
core.saveState('setSafeDirectory', 'true')
|
||||
}
|
||||
|
||||
// Publish a variable so that when the POST action runs, it can determine it should run the cleanup logic.
|
||||
// This is necessary since we don't have a separate entry point.
|
||||
if (!IsPost) {
|
||||
coreCommand.issueCommand('save-state', {name: 'isPost'}, 'true')
|
||||
core.saveState('isPost', 'true')
|
||||
}
|
||||
|
||||
88
src/unsafe-pr-checkout-helper.ts
Normal file
88
src/unsafe-pr-checkout-helper.ts
Normal file
@ -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())
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user