Skip to content

Commit d181798

Browse files
author
Alvaro Muñoz
committed
Split Cache Poisoning queries in 3
Split them into 3 queries depending of how the cache can be poisoned: - control of cached files - execution of controlled code - code injection Remove `setup-XXX` actions from CacheWriting class since the cached files are not in the CWD
1 parent fbc2e1e commit d181798

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+275
-582
lines changed

ql/src/Security/CWE-349/CachePoisoningByCodeInjection.ql renamed to ql/src/Security/CWE-349/CachePoisoningViaCodeInjection.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* @name Cache Poisoning via low-privilege code injection
2+
* @name Cache Poisoning via low-privileged code injection
33
* @description The cache can be poisoned by untrusted code, leading to a cache poisoning attack.
44
* @kind path-problem
55
* @problem.severity error

ql/src/Security/CWE-349/CachePoisoning.ql renamed to ql/src/Security/CWE-349/CachePoisoningViaDirectCache.ql

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/**
2-
* @name Cache Poisoning
2+
* @name Cache Poisoning via caching of untrusted files
33
* @description The cache can be poisoned by untrusted code, leading to a cache poisoning attack.
44
* @kind path-problem
55
* @problem.severity error
66
* @precision high
77
* @security-severity 7.5
8-
* @id actions/cache-poisoning
8+
* @id actions/cache-poisoning/direct-cache
99
* @tags actions
1010
* security
1111
* external/cwe/cwe-349
@@ -45,6 +45,8 @@ query predicate edges(Step a, Step b) { a.getNextStep() = b }
4545

4646
from LocalJob j, Event e, Step source, Step s, string message, string path
4747
where
48+
// the job checkouts untrusted code from a pull request or downloads an untrusted artifact
49+
j.getAStep() = source and
4850
(
4951
source instanceof PRHeadCheckoutStep and
5052
message = "due to privilege checkout of untrusted code." and
@@ -54,46 +56,35 @@ where
5456
message = "due to downloading an untrusted artifact." and
5557
path = source.(UntrustedArtifactDownloadStep).getPath()
5658
) and
59+
// the checkout/download is not controlled by an access check
60+
not exists(ControlCheck check | check.protects(source, j.getATriggerEvent())) and
5761
j.getATriggerEvent() = e and
5862
// job can be triggered by an external user
5963
e.isExternallyTriggerable() and
60-
// the checkout is not controlled by an access check
61-
not exists(ControlCheck check | check.protects(source, j.getATriggerEvent())) and
6264
(
6365
// the workflow runs in the context of the default branch
6466
runsOnDefaultBranch(e)
6567
or
66-
// the workflow caller runs in the context of the default branch
68+
// the workflow's caller runs in the context of the default branch
6769
e.getName() = "workflow_call" and
6870
exists(ExternalJob caller |
6971
caller.getCallee() = j.getLocation().getFile().getRelativePath() and
7072
runsOnDefaultBranch(caller.getATriggerEvent())
7173
)
7274
) and
73-
// the job checkouts untrusted code from a pull request
74-
j.getAStep() = source and
75+
// the job writes to the cache
76+
// (No need to follow the checkout/download step since the cache is normally write after the job completes)
77+
j.getAStep() = s and
78+
s instanceof CacheWritingStep and
7579
(
76-
// the job writes to the cache
77-
// (No need to follow the checkout step as the cache writing is normally done after the job completes)
78-
j.getAStep() = s and
79-
s instanceof CacheWritingStep and
80-
(
81-
// we dont know what code can be controlled by the attacker
82-
path = "?"
83-
or
84-
// we dont know what files are being cached
85-
s.(CacheWritingStep).getPath() = "?"
86-
or
87-
// the cache writing step reads from the path the attacker can control
88-
not path = "?" and controlledCachePath(s.(CacheWritingStep).getPath(), path)
89-
) and
90-
not s instanceof PoisonableStep
80+
// we dont know what code can be controlled by the attacker
81+
path = "?"
9182
or
92-
// the job executes checked-out code
93-
// (The cache specific token can be leaked even for non-privileged workflows)
94-
source.getAFollowingStep() = s and
95-
s instanceof PoisonableStep and
96-
// excluding privileged workflows since they can be exploited in easier circumstances
97-
not j.isPrivileged()
98-
)
83+
// we dont know what files are being cached
84+
s.(CacheWritingStep).getPath() = "?"
85+
or
86+
// the cache writing step reads from a path the attacker can control
87+
not path = "?" and controlledCachePath(s.(CacheWritingStep).getPath(), path)
88+
) and
89+
not s instanceof PoisonableStep
9990
select s, source, s, "Potential cache poisoning in the context of the default branch " + message
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @name Cache Poisoning via execution of untrusted code
3+
* @description The cache can be poisoned by untrusted code, leading to a cache poisoning attack.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @precision high
7+
* @security-severity 7.5
8+
* @id actions/cache-poisoning/poisonable-step
9+
* @tags actions
10+
* security
11+
* external/cwe/cwe-349
12+
*/
13+
14+
import actions
15+
import codeql.actions.security.ArtifactPoisoningQuery
16+
import codeql.actions.security.UntrustedCheckoutQuery
17+
import codeql.actions.security.CachePoisoningQuery
18+
import codeql.actions.security.PoisonableSteps
19+
import codeql.actions.security.ControlChecks
20+
21+
query predicate edges(Step a, Step b) { a.getNextStep() = b }
22+
23+
from LocalJob j, Event e, Step source, Step s, string message, string path
24+
where
25+
// the job checkouts untrusted code from a pull request or downloads an untrusted artifact
26+
j.getAStep() = source and
27+
(
28+
source instanceof PRHeadCheckoutStep and
29+
message = "due to privilege checkout of untrusted code." and
30+
path = source.(PRHeadCheckoutStep).getPath()
31+
or
32+
source instanceof UntrustedArtifactDownloadStep and
33+
message = "due to downloading an untrusted artifact." and
34+
path = source.(UntrustedArtifactDownloadStep).getPath()
35+
) and
36+
// the checkout/download is not controlled by an access check
37+
not exists(ControlCheck check | check.protects(source, j.getATriggerEvent())) and
38+
j.getATriggerEvent() = e and
39+
// job can be triggered by an external user
40+
e.isExternallyTriggerable() and
41+
(
42+
// the workflow runs in the context of the default branch
43+
runsOnDefaultBranch(e)
44+
or
45+
// the workflow's caller runs in the context of the default branch
46+
e.getName() = "workflow_call" and
47+
exists(ExternalJob caller |
48+
caller.getCallee() = j.getLocation().getFile().getRelativePath() and
49+
runsOnDefaultBranch(caller.getATriggerEvent())
50+
)
51+
) and
52+
// the job executes checked-out code
53+
// (The cache specific token can be leaked even for non-privileged workflows)
54+
source.getAFollowingStep() = s and
55+
s instanceof PoisonableStep and
56+
// excluding privileged workflows since they can be exploited in easier circumstances
57+
not j.isPrivileged()
58+
select s, source, s, "Potential cache poisoning in the context of the default branch " + message
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: Test
2+
3+
on:
4+
pull_request_target:
5+
branches: [ master, main, dev ]
6+
7+
jobs:
8+
test:
9+
name: Test
10+
runs-on: ubuntu-latest
11+
steps:
12+
- id: modified_files
13+
uses: trilom/[email protected]
14+
with:
15+
output: ","
16+
- run: echo "${{ steps.modified_files.outputs.files_modified }}"

0 commit comments

Comments
 (0)