Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
# running from unless specified. Example URLs are https://github.com or
# https://my-ghes-server.example.com
github-server-url: ''

# Suppress the warning when pull_request_target checks out a non-default branch
# from the workflow repository. Only set this to true when you understand the
# security risk of running untrusted pull request code in a privileged context.
# https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/
# Default: false
dangerously-checkout-non-default-branch: ''
```
<!-- end usage -->

Expand Down
80 changes: 80 additions & 0 deletions __test__/input-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ describe('input-helper tests', () => {
beforeEach(() => {
// Reset inputs
inputs = {}
github.context.eventName = 'push'
github.context.ref = 'refs/heads/some-ref'
github.context.sha = '1234567890123456789012345678901234567890'
github.context.payload = {
repository: {
default_branch: 'main'
}
} as any
jest.clearAllMocks()
})

afterAll(() => {
Expand All @@ -65,6 +74,8 @@ describe('input-helper tests', () => {
}

// Restore @actions/github context
github.context.eventName = originalContext.eventName
github.context.payload = originalContext.payload
github.context.ref = originalContext.ref
github.context.sha = originalContext.sha

Expand Down Expand Up @@ -150,6 +161,75 @@ describe('input-helper tests', () => {
expect(settings.commit).toBeFalsy()
})

it('warns when pull_request_target checks out a non-default branch', async () => {
github.context.eventName = 'pull_request_target'
inputs.ref = 'some-other-ref'

await inputHelper.getInputs()

expect(core.warning).toHaveBeenCalledWith(
expect.stringContaining(
'Checking out a non-default branch from pull_request_target'
)
)
})

it('does not warn when pull_request_target checks out the default branch name', async () => {
github.context.eventName = 'pull_request_target'
inputs.ref = 'main'

await inputHelper.getInputs()

expect(core.warning).not.toHaveBeenCalled()
})

it('does not warn when pull_request_target checks out the fully qualified default branch', async () => {
github.context.eventName = 'pull_request_target'
inputs.ref = 'refs/heads/main'

await inputHelper.getInputs()

expect(core.warning).not.toHaveBeenCalled()
})

it('does not warn when pull_request_target checks out the default branch sha', async () => {
github.context.eventName = 'pull_request_target'
inputs.ref = '1234567890123456789012345678901234567890'

await inputHelper.getInputs()

expect(core.warning).not.toHaveBeenCalled()
})
Comment thread
KengoTODA marked this conversation as resolved.

it('does not warn when dangerously-checkout-non-default-branch suppresses the warning', async () => {
github.context.eventName = 'pull_request_target'
inputs.ref = 'some-other-ref'
inputs['dangerously-checkout-non-default-branch'] = 'true'

await inputHelper.getInputs()

expect(core.warning).not.toHaveBeenCalled()
})

it('does not warn when pull_request checks out a non-default branch', async () => {
github.context.eventName = 'pull_request'
inputs.ref = 'some-other-ref'

await inputHelper.getInputs()

expect(core.warning).not.toHaveBeenCalled()
})

it('does not warn when pull_request_target checks out a different repository', async () => {
github.context.eventName = 'pull_request_target'
inputs.repository = 'some-owner/some-other-repo'
inputs.ref = 'some-other-ref'

await inputHelper.getInputs()

expect(core.warning).not.toHaveBeenCalled()
})

it('sets workflow organization ID', async () => {
const settings: IGitSourceSettings = await inputHelper.getInputs()
expect(settings.workflowOrganizationId).toBe(123456)
Expand Down
8 changes: 8 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ inputs:
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
required: false
dangerously-checkout-non-default-branch:
description: >
Suppress the warning when pull_request_target checks out a non-default
branch from the workflow repository. Only set this to true when you
understand the security risk of running untrusted pull request code in a
privileged context.
https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/
default: false
outputs:
ref:
description: 'The branch, tag or SHA that was checked out'
Expand Down
22 changes: 21 additions & 1 deletion dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2008,7 +2008,8 @@ function getInputs() {
const isWorkflowRepository = qualifiedRepository.toUpperCase() ===
`${github.context.repo.owner}/${github.context.repo.repo}`.toUpperCase();
// Source branch, source version
result.ref = core.getInput('ref');
const inputRef = core.getInput('ref');
result.ref = inputRef;
if (!result.ref) {
if (isWorkflowRepository) {
result.ref = github.context.ref;
Expand All @@ -2027,6 +2028,16 @@ function getInputs() {
}
core.debug(`ref = '${result.ref}'`);
core.debug(`commit = '${result.commit}'`);
// Warn when pull_request_target checks out non-default code from the workflow repository.
// This event runs in the base repository context, so checking out PR-controlled code can be risky.
const suppressNonDefaultBranchWarning = (core.getInput('dangerously-checkout-non-default-branch') || 'false').toUpperCase() === 'TRUE';
if (github.context.eventName === 'pull_request_target' &&
isWorkflowRepository &&
inputRef &&
!suppressNonDefaultBranchWarning &&
!isDefaultBranchRef(inputRef)) {
core.warning('Checking out a non-default branch from pull_request_target can put untrusted pull request code in a privileged context. If this is intentional, set dangerously-checkout-non-default-branch: true. Consider using pull_request or pull_request plus workflow_run instead. See https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/');
}
// Clean
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE';
core.debug(`clean = ${result.clean}`);
Expand Down Expand Up @@ -2098,6 +2109,15 @@ function getInputs() {
return result;
});
}
function isDefaultBranchRef(ref) {
var _a;
const defaultBranch = (_a = github.context.payload.repository) === null || _a === void 0 ? void 0 : _a.default_branch;
if (defaultBranch &&
(ref === defaultBranch || ref === `refs/heads/${defaultBranch}`)) {
return true;
}
return ref.toUpperCase() === github.context.sha.toUpperCase();
}


/***/ }),
Expand Down
34 changes: 33 additions & 1 deletion src/input-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export async function getInputs(): Promise<IGitSourceSettings> {
`${github.context.repo.owner}/${github.context.repo.repo}`.toUpperCase()

// Source branch, source version
result.ref = core.getInput('ref')
const inputRef = core.getInput('ref')
result.ref = inputRef
if (!result.ref) {
if (isWorkflowRepository) {
result.ref = github.context.ref
Expand All @@ -78,6 +79,24 @@ export async function getInputs(): Promise<IGitSourceSettings> {
core.debug(`ref = '${result.ref}'`)
core.debug(`commit = '${result.commit}'`)

// Warn when pull_request_target checks out non-default code from the workflow repository.
// This event runs in the base repository context, so checking out PR-controlled code can be risky.
const suppressNonDefaultBranchWarning =
(
core.getInput('dangerously-checkout-non-default-branch') || 'false'
).toUpperCase() === 'TRUE'
if (
github.context.eventName === 'pull_request_target' &&
isWorkflowRepository &&
inputRef &&
!suppressNonDefaultBranchWarning &&
!isDefaultBranchRef(inputRef)
) {
core.warning(
'Checking out a non-default branch from pull_request_target can put untrusted pull request code in a privileged context. If this is intentional, set dangerously-checkout-non-default-branch: true. Consider using pull_request or pull_request plus workflow_run instead. See https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/'
)
}

// Clean
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'
core.debug(`clean = ${result.clean}`)
Expand Down Expand Up @@ -163,3 +182,16 @@ export async function getInputs(): Promise<IGitSourceSettings> {

return result
}

function isDefaultBranchRef(ref: string): boolean {
const defaultBranch = (github.context.payload.repository as any)
?.default_branch
if (
defaultBranch &&
(ref === defaultBranch || ref === `refs/heads/${defaultBranch}`)
) {
return true
}

return ref.toUpperCase() === github.context.sha.toUpperCase()
}
Comment thread
KengoTODA marked this conversation as resolved.
Loading