Skip to content

Commit 857758f

Browse files
committed
Preserve execution path provenance
1 parent a48c857 commit 857758f

4 files changed

Lines changed: 70 additions & 14 deletions

File tree

actions/ql/lib/codeql/actions/security/PoisonableSteps.qll

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ class LocalScriptExecutionRunStep extends PoisonableStep, Run {
4646
)
4747
}
4848

49-
string getPath() { result = normalizePath(path.splitAt(" ")) }
49+
/** Gets the captured path before workspace qualification. */
50+
string getRawPath() { result = path.splitAt(" ") }
51+
52+
string getPath() { result = normalizePath(this.getRawPath()) }
5053
}
5154

5255
class LocalActionUsesStep extends PoisonableStep, UsesStep {

actions/ql/src/Security/CWE-829/UntrustedCheckoutCritical.ql

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,9 @@ private class ActionsCheckoutPathInput extends NormalizableFilepath {
3333

3434
private class ExecutionPathInput extends NormalizableFilepath {
3535
ExecutionPathInput() {
36-
exists(LocalScriptExecutionRunStep step | this = step.getPath())
36+
exists(LocalScriptExecutionRunStep step | this = trimQuotes(step.getRawPath()))
3737
or
38-
exists(LocalActionUsesStep step | this = step.getPath())
39-
or
40-
this = "GITHUB_WORKSPACE/"
38+
exists(LocalActionUsesStep step | this = step.getCallee())
4139
}
4240
}
4341

@@ -60,19 +58,26 @@ private string getNormalizedActionsCheckoutPath(PRHeadCheckoutStep checkout) {
6058
bindingset[path]
6159
private string getNormalizedExecutionPath(string path) {
6260
exists(ExecutionPathInput executionPath, string normalized |
63-
executionPath = path and
61+
executionPath = trimQuotes(path) and
6462
not executionPath.regexpMatch(".*\\$\\{\\{.*") and
6563
normalized = executionPath.getNormalizedPath() and
66-
(normalized = "GITHUB_WORKSPACE" or normalized.matches("GITHUB_WORKSPACE/%")) and
67-
result = normalized
64+
not normalized.matches("/%") and
65+
normalized != ".." and
66+
not normalized.matches("../%") and
67+
if normalized = "."
68+
then result = "GITHUB_WORKSPACE"
69+
else (
70+
normalized.regexpMatch("^[^$/~].*") and
71+
result = "GITHUB_WORKSPACE/" + normalized
72+
)
6873
)
6974
}
7075

71-
bindingset[checkout, path]
72-
private predicate checkoutContainsPath(PRHeadCheckoutStep checkout, string path) {
76+
bindingset[checkout, rawPath, path]
77+
private predicate checkoutContainsPath(PRHeadCheckoutStep checkout, string rawPath, string path) {
7378
exists(string root, string candidate |
7479
root = getNormalizedActionsCheckoutPath(checkout) and
75-
candidate = getNormalizedExecutionPath(path) and
80+
candidate = getNormalizedExecutionPath(rawPath) and
7681
// Canonicalize both paths so dot segments cannot enter or escape the checkout while still
7782
// passing a lexical prefix check.
7883
(candidate = root or candidate.indexOf(root + "/") = 0)
@@ -98,7 +103,8 @@ where
98103
(
99104
// Check if the poisonable step is a local script execution step
100105
// and the path of the command or script matches the path of the downloaded artifact
101-
checkoutContainsPath(checkout, poisonable.(LocalScriptExecutionRunStep).getPath())
106+
checkoutContainsPath(checkout, poisonable.(LocalScriptExecutionRunStep).getRawPath(),
107+
poisonable.(LocalScriptExecutionRunStep).getPath())
102108
or
103109
// Checking the path for non local script execution steps is very difficult
104110
not poisonable instanceof LocalScriptExecutionRunStep
@@ -111,7 +117,8 @@ where
111117
not poisonable instanceof LocalActionUsesStep and
112118
checkoutUsesWorkspaceRoot(checkout)
113119
or
114-
checkoutContainsPath(checkout, poisonable.(LocalActionUsesStep).getPath())
120+
checkoutContainsPath(checkout, poisonable.(LocalActionUsesStep).getCallee(),
121+
poisonable.(LocalActionUsesStep).getPath())
115122
)
116123
) and
117124
// the checkout occurs in a privileged context

actions/ql/test/query-tests/Security/CWE-829/.github/workflows/untrusted_checkout_paths.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,43 @@ jobs:
146146
ref: ${{ github.event.pull_request.head.sha }}
147147
path: a/b
148148
- run: ./a/b-other/build.sh
149+
150+
literal-workspace-component-bare:
151+
runs-on: ubuntu-latest
152+
steps:
153+
- uses: actions/checkout@v4.2.2
154+
with:
155+
repository: ${{ github.event.pull_request.head.repo.full_name }}
156+
ref: ${{ github.event.pull_request.head.sha }}
157+
path: GITHUB_WORKSPACE/checkout
158+
- run: bash GITHUB_WORKSPACE/checkout/build.sh
159+
160+
literal-workspace-component-dot:
161+
runs-on: ubuntu-latest
162+
steps:
163+
- uses: actions/checkout@v4.2.2
164+
with:
165+
repository: ${{ github.event.pull_request.head.repo.full_name }}
166+
ref: ${{ github.event.pull_request.head.sha }}
167+
path: GITHUB_WORKSPACE/checkout
168+
- run: bash ./GITHUB_WORKSPACE/checkout/build.sh
169+
170+
rooted-workspace-spelling-bare:
171+
runs-on: ubuntu-latest
172+
steps:
173+
- uses: actions/checkout@v4.2.2
174+
with:
175+
repository: ${{ github.event.pull_request.head.repo.full_name }}
176+
ref: ${{ github.event.pull_request.head.sha }}
177+
path: candidate
178+
- run: bash GITHUB_WORKSPACE/candidate/build.sh
179+
180+
rooted-workspace-spelling-dot:
181+
runs-on: ubuntu-latest
182+
steps:
183+
- uses: actions/checkout@v4.2.2
184+
with:
185+
repository: ${{ github.event.pull_request.head.repo.full_name }}
186+
ref: ${{ github.event.pull_request.head.sha }}
187+
path: candidate
188+
- run: bash ./GITHUB_WORKSPACE/candidate/build.sh

actions/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutCritical.expected

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,11 @@ edges
351351
| .github/workflows/untrusted_checkout_paths.yml:110:9:115:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:115:9:119:2 | Uses Step |
352352
| .github/workflows/untrusted_checkout_paths.yml:122:9:126:6 | Run Step | .github/workflows/untrusted_checkout_paths.yml:126:9:130:2 | Uses Step |
353353
| .github/workflows/untrusted_checkout_paths.yml:133:9:138:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:138:9:140:2 | Run Step |
354-
| .github/workflows/untrusted_checkout_paths.yml:143:9:148:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:148:9:148:34 | Run Step |
354+
| .github/workflows/untrusted_checkout_paths.yml:143:9:148:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:148:9:150:2 | Run Step |
355+
| .github/workflows/untrusted_checkout_paths.yml:153:9:158:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:158:9:160:2 | Run Step |
356+
| .github/workflows/untrusted_checkout_paths.yml:163:9:168:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:168:9:170:2 | Run Step |
357+
| .github/workflows/untrusted_checkout_paths.yml:173:9:178:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:178:9:180:2 | Run Step |
358+
| .github/workflows/untrusted_checkout_paths.yml:183:9:188:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:188:9:188:56 | Run Step |
355359
| .github/workflows/untrusted_checkout_permission_check_reusable2.yml:8:9:16:6 | Uses Step: checkAccess | .github/workflows/untrusted_checkout_permission_check_reusable2.yml:16:9:22:2 | Run Step |
356360
| .github/workflows/untrusted_checkout_permission_check_reusable.yml:8:9:16:6 | Uses Step: checkAccess | .github/workflows/untrusted_checkout_permission_check_reusable.yml:16:9:22:2 | Run Step |
357361
| .github/workflows/untrusted_checkout_permission_check_reusable_level2.yml:8:9:16:6 | Uses Step: checkAccess | .github/workflows/untrusted_checkout_permission_check_reusable_level2.yml:16:9:22:2 | Run Step |
@@ -418,5 +422,7 @@ edges
418422
| .github/workflows/untrusted_checkout_paths.yml:80:9:85:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:80:9:85:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:85:9:87:2 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/untrusted_checkout_paths.yml:4:3:4:21 | pull_request_target | pull_request_target |
419423
| .github/workflows/untrusted_checkout_paths.yml:100:9:105:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:100:9:105:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:105:9:107:2 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/untrusted_checkout_paths.yml:4:3:4:21 | pull_request_target | pull_request_target |
420424
| .github/workflows/untrusted_checkout_paths.yml:110:9:115:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:110:9:115:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:115:9:119:2 | Uses Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/untrusted_checkout_paths.yml:4:3:4:21 | pull_request_target | pull_request_target |
425+
| .github/workflows/untrusted_checkout_paths.yml:153:9:158:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:153:9:158:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:158:9:160:2 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/untrusted_checkout_paths.yml:4:3:4:21 | pull_request_target | pull_request_target |
426+
| .github/workflows/untrusted_checkout_paths.yml:163:9:168:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:163:9:168:6 | Uses Step | .github/workflows/untrusted_checkout_paths.yml:168:9:170:2 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/untrusted_checkout_paths.yml:4:3:4:21 | pull_request_target | pull_request_target |
421427
| .github/workflows/untrusted_checkout_permissions_check.yml:36:9:41:6 | Uses Step | .github/workflows/untrusted_checkout_permissions_check.yml:36:9:41:6 | Uses Step | .github/workflows/untrusted_checkout_permissions_check.yml:41:9:41:22 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/untrusted_checkout_permissions_check.yml:2:3:2:21 | pull_request_target | pull_request_target |
422428
| .github/workflows/workflow_run_untrusted_checkout_path.yml:12:9:16:6 | Uses Step | .github/workflows/workflow_run_untrusted_checkout_path.yml:12:9:16:6 | Uses Step | .github/workflows/workflow_run_untrusted_checkout_path.yml:16:9:16:40 | Run Step | Checkout of untrusted code in a privileged workflow with later potential execution (event trigger: $@). | .github/workflows/workflow_run_untrusted_checkout_path.yml:4:3:4:14 | workflow_run | workflow_run |

0 commit comments

Comments
 (0)