Compare commits

..

5 Commits

Author SHA1 Message Date
Aiqiao Yan
34ec4af9e0 update license 2026-06-16 17:06:46 +00:00
Aiqiao Yan
b34efac05f switch to use crpto.randomUUID 2026-06-16 17:02:16 +00:00
dependabot[bot]
6f1b2974c7 Bump uuid, @actions/core and @actions/tool-cache
Bumps [uuid](https://github.com/uuidjs/uuid), [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) and [@actions/tool-cache](https://github.com/actions/toolkit/tree/HEAD/packages/tool-cache). These dependencies needed to be updated together.

Updates `uuid` from 9.0.1 to 14.0.0
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v9.0.1...v14.0.0)

Updates `@actions/core` from 1.10.1 to 1.11.1
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core)

Updates `@actions/tool-cache` from 2.0.1 to 2.0.2
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/tool-cache/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/tool-cache)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 14.0.0
  dependency-type: direct:production
- dependency-name: "@actions/core"
  dependency-version: 1.11.1
  dependency-type: direct:production
- dependency-name: "@actions/tool-cache"
  dependency-version: 2.0.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 16:34:56 +00:00
dependabot[bot]
0f9f3aa320 Bump actions/publish-immutable-action (#2458)
Bumps the minor-actions-dependencies group with 1 update in the / directory: [actions/publish-immutable-action](https://github.com/actions/publish-immutable-action).


Updates `actions/publish-immutable-action` from 0.0.3 to 0.0.4
- [Release notes](https://github.com/actions/publish-immutable-action/releases)
- [Commits](https://github.com/actions/publish-immutable-action/compare/0.0.3...v0.0.4)

---
updated-dependencies:
- dependency-name: actions/publish-immutable-action
  dependency-version: 0.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 12:33:25 -04:00
Aiqiao Yan
f9e715a95f block checking out fork pr for pull_request_target and workflow_run (#2454)
* block checking out fork pr for some events

* address copilot and reviewer feedback

* run prettier formatting

* build

* update urls

* update readme

* update description and url again

* edit url one more time
2026-06-16 10:03:43 -04:00
20 changed files with 740 additions and 1634 deletions

View File

@@ -17,4 +17,4 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Publish - name: Publish
id: publish id: publish
uses: actions/publish-immutable-action@0.0.3 uses: actions/publish-immutable-action@v0.0.4

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/core" name: "@actions/core"
version: 1.10.1 version: 1.11.1
type: npm type: npm
summary: Actions core lib summary: Actions core lib
homepage: https://github.com/actions/toolkit/tree/main/packages/core homepage: https://github.com/actions/toolkit/tree/main/packages/core

View File

@@ -1,6 +1,6 @@
--- ---
name: "@actions/tool-cache" name: "@actions/tool-cache"
version: 2.0.1 version: 2.0.2
type: npm type: npm
summary: Actions tool-cache lib summary: Actions tool-cache lib
homepage: https://github.com/actions/toolkit/tree/main/packages/tool-cache homepage: https://github.com/actions/toolkit/tree/main/packages/tool-cache

View File

@@ -1,39 +0,0 @@
---
name: uuid
version: 3.4.0
type: npm
summary: RFC4122 (v1, v4, and v5) UUIDs
homepage:
license: mit
licenses:
- sources: LICENSE.md
text: |
The MIT License (MIT)
Copyright (c) 2010-2016 Robert Kieffer and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
notices:
- sources: AUTHORS
text: |-
Robert Kieffer <robert@broofa.com>
Christoph Tavan <dev@tavan.de>
AJ ONeal <coolaj86@gmail.com>
Vincent Voyer <vincent@zeroload.net>
Roman Shtylman <shtylman@gmail.com>

View File

@@ -1,20 +0,0 @@
---
name: uuid
version: 8.3.2
type: npm
summary: RFC4122 (v1, v4, and v5) UUIDs
homepage: https://github.com/uuidjs/uuid#readme
license: mit
licenses:
- sources: LICENSE.md
text: |
The MIT License (MIT)
Copyright (c) 2010-2020 Robert Kieffer and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
notices: []

View File

@@ -1,20 +0,0 @@
---
name: uuid
version: 9.0.1
type: npm
summary: RFC4122 (v1, v4, and v5) UUIDs
homepage:
license: mit
licenses:
- sources: LICENSE.md
text: |
The MIT License (MIT)
Copyright (c) 2010-2020 Robert Kieffer and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
notices: []

View File

@@ -160,6 +160,15 @@ 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`. 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 --> <!-- end usage -->

View File

@@ -1173,7 +1173,8 @@ async function setup(testName: string): Promise<void> {
sshUser: '', sshUser: '',
workflowOrganizationId: 123456, workflowOrganizationId: 123456,
setSafeDirectory: true, setSafeDirectory: true,
githubServerUrl: githubServerUrl githubServerUrl: githubServerUrl,
allowUnsafePrCheckout: false
} }
} }

View File

@@ -91,6 +91,7 @@ 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 () => {

View 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()
})
})

View File

@@ -98,6 +98,15 @@ 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`. 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
outputs: outputs:
ref: ref:
description: 'The branch, tag or SHA that was checked out' description: 'The branch, tag or SHA that was checked out'

1808
dist/index.js vendored

File diff suppressed because it is too large Load Diff

64
package-lock.json generated
View File

@@ -9,17 +9,15 @@
"version": "5.0.0", "version": "5.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1", "@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0", "@actions/github": "^6.0.0",
"@actions/io": "^1.1.3", "@actions/io": "^1.1.3",
"@actions/tool-cache": "^2.0.1", "@actions/tool-cache": "^2.0.2"
"uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/node": "^24.1.0", "@types/node": "^24.1.0",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.9.0", "@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0", "@typescript-eslint/parser": "^7.9.0",
"@vercel/ncc": "^0.38.1", "@vercel/ncc": "^0.38.1",
@@ -44,20 +42,13 @@
} }
}, },
"node_modules/@actions/core": { "node_modules/@actions/core": {
"version": "1.10.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
"integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
"license": "MIT",
"dependencies": { "dependencies": {
"@actions/http-client": "^2.0.1", "@actions/exec": "^1.1.1",
"uuid": "^8.3.2" "@actions/http-client": "^2.0.1"
}
},
"node_modules/@actions/core/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
} }
}, },
"node_modules/@actions/exec": { "node_modules/@actions/exec": {
@@ -94,25 +85,16 @@
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="
}, },
"node_modules/@actions/tool-cache": { "node_modules/@actions/tool-cache": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-2.0.2.tgz",
"integrity": "sha512-iPU+mNwrbA8jodY8eyo/0S/QqCKDajiR8OxWTnSk/SnYg0sj8Hp4QcUEVC1YFpHWXtrfbQrE13Jz4k4HXJQKcA==", "integrity": "sha512-fBhNNOWxuoLxztQebpOaWu6WeVmuwa77Z+DxIZ1B+OYvGkGQon6kTVg6Z32Cb13WCuw0szqonK+hh03mJV7Z6w==",
"license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.2.6", "@actions/core": "^1.11.1",
"@actions/exec": "^1.0.0", "@actions/exec": "^1.0.0",
"@actions/http-client": "^2.0.1", "@actions/http-client": "^2.0.1",
"@actions/io": "^1.1.1", "@actions/io": "^1.1.1",
"semver": "^6.1.0", "semver": "^6.1.0"
"uuid": "^3.3.2"
}
},
"node_modules/@actions/tool-cache/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@@ -1529,12 +1511,6 @@
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"dev": true "dev": true
}, },
"node_modules/@types/uuid": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
"dev": true
},
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
"version": "17.0.32", "version": "17.0.32",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
@@ -6914,18 +6890,6 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-to-istanbul": { "node_modules/v8-to-istanbul": {
"version": "9.2.0", "version": "9.2.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz",

View File

@@ -28,17 +28,15 @@
}, },
"homepage": "https://github.com/actions/checkout#readme", "homepage": "https://github.com/actions/checkout#readme",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1", "@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0", "@actions/github": "^6.0.0",
"@actions/io": "^1.1.3", "@actions/io": "^1.1.3",
"@actions/tool-cache": "^2.0.1", "@actions/tool-cache": "^2.0.2"
"uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/node": "^24.1.0", "@types/node": "^24.1.0",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.9.0", "@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0", "@typescript-eslint/parser": "^7.9.0",
"@vercel/ncc": "^0.38.1", "@vercel/ncc": "^0.38.1",

View File

@@ -8,7 +8,7 @@ import * as path from 'path'
import * as regexpHelper from './regexp-helper' import * as regexpHelper from './regexp-helper'
import * as stateHelper from './state-helper' import * as stateHelper from './state-helper'
import * as urlHelper from './url-helper' import * as urlHelper from './url-helper'
import {v4 as uuid} from 'uuid' import {randomUUID} from 'crypto'
import {IGitCommandManager} from './git-command-manager' import {IGitCommandManager} from './git-command-manager'
import {IGitSourceSettings} from './git-source-settings' import {IGitSourceSettings} from './git-source-settings'
@@ -90,7 +90,7 @@ class GitAuthHelper {
// Create a temp home directory // Create a temp home directory
const runnerTemp = process.env['RUNNER_TEMP'] || '' const runnerTemp = process.env['RUNNER_TEMP'] || ''
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined') assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
const uniqueId = uuid() const uniqueId = randomUUID()
this.temporaryHomePath = path.join(runnerTemp, uniqueId) this.temporaryHomePath = path.join(runnerTemp, uniqueId)
await fs.promises.mkdir(this.temporaryHomePath, {recursive: true}) await fs.promises.mkdir(this.temporaryHomePath, {recursive: true})
@@ -255,7 +255,7 @@ class GitAuthHelper {
// Write key // Write key
const runnerTemp = process.env['RUNNER_TEMP'] || '' const runnerTemp = process.env['RUNNER_TEMP'] || ''
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined') assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
const uniqueId = uuid() const uniqueId = randomUUID()
this.sshKeyPath = path.join(runnerTemp, uniqueId) this.sshKeyPath = path.join(runnerTemp, uniqueId)
stateHelper.setSshKeyPath(this.sshKeyPath) stateHelper.setSshKeyPath(this.sshKeyPath)
await fs.promises.mkdir(runnerTemp, {recursive: true}) await fs.promises.mkdir(runnerTemp, {recursive: true})
@@ -422,7 +422,7 @@ class GitAuthHelper {
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined') assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
// Create a unique filename for this checkout instance // Create a unique filename for this checkout instance
const configFileName = `git-credentials-${uuid()}.config` const configFileName = `git-credentials-${randomUUID()}.config`
this.credentialsConfigPath = path.join(runnerTemp, configFileName) this.credentialsConfigPath = path.join(runnerTemp, configFileName)
core.debug(`Credentials config path: ${this.credentialsConfigPath}`) core.debug(`Credentials config path: ${this.credentialsConfigPath}`)

View File

@@ -118,4 +118,10 @@ 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
} }

View File

@@ -6,7 +6,7 @@ import * as io from '@actions/io'
import * as path from 'path' import * as path from 'path'
import * as retryHelper from './retry-helper' import * as retryHelper from './retry-helper'
import * as toolCache from '@actions/tool-cache' import * as toolCache from '@actions/tool-cache'
import {v4 as uuid} from 'uuid' import {randomUUID} from 'crypto'
import {getServerApiUrl} from './url-helper' import {getServerApiUrl} from './url-helper'
const IS_WINDOWS = process.platform === 'win32' const IS_WINDOWS = process.platform === 'win32'
@@ -39,7 +39,7 @@ export async function downloadRepository(
// Write archive to disk // Write archive to disk
core.info('Writing archive to disk') core.info('Writing archive to disk')
const uniqueId = uuid() const uniqueId = randomUUID()
const archivePath = IS_WINDOWS const archivePath = IS_WINDOWS
? path.join(repositoryPath, `${uniqueId}.zip`) ? path.join(repositoryPath, `${uniqueId}.zip`)
: path.join(repositoryPath, `${uniqueId}.tar.gz`) : path.join(repositoryPath, `${uniqueId}.tar.gz`)

View File

@@ -2,6 +2,7 @@ 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'
@@ -161,5 +162,18 @@ 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
} }

View File

@@ -292,7 +292,7 @@ export async function checkCommitInfo(
} }
} }
function fromPayload(path: string): any { export function fromPayload(path: string): any {
return select(github.context.payload, path) return select(github.context.payload, path)
} }

View 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 after reviewing ` +
`the risks at https://gh.io/securely-using-pull_request_target, 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())
}
}