feat: warn on non-default checkout during pull_request_target

Signed-off-by: Kengo TODA <skypencil@gmail.com>
This commit is contained in:
Kengo TODA 2026-05-13 08:32:55 +08:00
parent 900f2210b1
commit 5a3004714a
No known key found for this signature in database
5 changed files with 149 additions and 2 deletions

View File

@ -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 -->

View File

@ -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(() => {
@ -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
@ -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()
})
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)

View File

@ -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'

22
dist/index.js vendored
View File

@ -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;
@ -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}`);
@ -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();
}
/***/ }),

View File

@ -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
@ -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}`)
@ -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()
}