@@ -18,19 +18,47 @@ import codeql.actions.security.CachePoisoningQuery
18
18
import codeql.actions.security.PoisonableSteps
19
19
import codeql.actions.security.ControlChecks
20
20
21
+ /**
22
+ * Holds if the path cache_path is a subpath of the path untrusted_path.
23
+ */
24
+ bindingset [ cache_path, untrusted_path]
25
+ predicate controlledCachePath ( string cache_path , string untrusted_path ) {
26
+ exists ( string normalized_cache_path , string normalized_untrusted_path |
27
+ (
28
+ cache_path .regexpMatch ( "^[a-zA-Z0-9_-].*" ) and
29
+ normalized_cache_path = "./" + cache_path .regexpReplaceAll ( "/$" , "" )
30
+ or
31
+ normalized_cache_path = cache_path .regexpReplaceAll ( "/$" , "" )
32
+ ) and
33
+ (
34
+ untrusted_path .regexpMatch ( "^[a-zA-Z0-9_-].*" ) and
35
+ normalized_untrusted_path = "./" + untrusted_path .regexpReplaceAll ( "/$" , "" )
36
+ or
37
+ normalized_untrusted_path = untrusted_path .regexpReplaceAll ( "/$" , "" )
38
+ ) and
39
+ normalized_cache_path .substring ( 0 , normalized_untrusted_path .length ( ) ) =
40
+ normalized_untrusted_path
41
+ )
42
+ }
43
+
21
44
query predicate edges ( Step a , Step b ) { a .getNextStep ( ) = b }
22
45
23
- from LocalJob j , Event e , Step artifact , Step s
46
+ from LocalJob j , Event e , Step source , Step s , string message , string path
24
47
where
25
48
(
26
- artifact instanceof PRHeadCheckoutStep or
27
- artifact instanceof UntrustedArtifactDownloadStep
49
+ source instanceof PRHeadCheckoutStep and
50
+ message = "due to privilege checkout of untrusted code." and
51
+ path = source .( PRHeadCheckoutStep ) .getPath ( )
52
+ or
53
+ source instanceof UntrustedArtifactDownloadStep and
54
+ message = "due to downloading an untrusted artifact." and
55
+ path = source .( UntrustedArtifactDownloadStep ) .getPath ( )
28
56
) and
29
57
j .getATriggerEvent ( ) = e and
30
58
// job can be triggered by an external user
31
59
e .isExternallyTriggerable ( ) and
32
60
// the checkout is not controlled by an access check
33
- not exists ( ControlCheck check | check .protects ( artifact , j .getATriggerEvent ( ) ) ) and
61
+ not exists ( ControlCheck check | check .protects ( source , j .getATriggerEvent ( ) ) ) and
34
62
(
35
63
// the workflow runs in the context of the default branch
36
64
runsOnDefaultBranch ( e )
@@ -43,19 +71,29 @@ where
43
71
)
44
72
) and
45
73
// the job checkouts untrusted code from a pull request
46
- j .getAStep ( ) = artifact and
74
+ j .getAStep ( ) = source and
47
75
(
48
76
// the job writes to the cache
49
77
// (No need to follow the checkout step as the cache writing is normally done after the job completes)
50
78
j .getAStep ( ) = s and
51
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
52
90
not s instanceof PoisonableStep
53
91
or
54
92
// the job executes checked-out code
55
93
// (The cache specific token can be leaked even for non-privileged workflows)
56
- artifact .getAFollowingStep ( ) = s and
94
+ source .getAFollowingStep ( ) = s and
57
95
s instanceof PoisonableStep and
58
96
// excluding privileged workflows since they can be exploited in easier circumstances
59
97
not j .isPrivileged ( )
60
98
)
61
- select s , artifact , s , "Potential cache poisoning in the context of the default branch"
99
+ select s , source , s , "Potential cache poisoning in the context of the default branch" + message
0 commit comments