1414 */
1515
1616import actions
17+ private import codeql.util.FilePath
1718import codeql.actions.security.UntrustedCheckoutQuery
1819import codeql.actions.security.PoisonableSteps
1920import codeql.actions.security.ControlChecks
2021
2122query predicate edges ( Step a , Step b ) { a .getNextStep ( ) = b }
2223
24+ private class ActionsCheckoutPathInput extends NormalizableFilepath {
25+ ActionsCheckoutPathInput ( ) {
26+ exists ( PRHeadCheckoutStep checkout |
27+ checkout instanceof UsesStep and
28+ checkout .( UsesStep ) .getCallee ( ) = "actions/checkout" and
29+ this = trimQuotes ( checkout .( UsesStep ) .getArgument ( "path" ) )
30+ )
31+ }
32+ }
33+
34+ /** Gets the modeled script operand before shared path normalization can discard dot segments. */
35+ private string getUnnormalizedLocalScriptPath ( LocalScriptExecutionRunStep step ) {
36+ exists ( string regexp , int pathGroup |
37+ poisonableLocalScriptsDataModel ( regexp , pathGroup ) and
38+ result = step .getScript ( ) .getACommand ( ) .regexpCapture ( regexp , pathGroup ) .splitAt ( " " )
39+ )
40+ }
41+
42+ private class ExecutionPathInput extends NormalizableFilepath {
43+ ExecutionPathInput ( ) {
44+ exists ( LocalScriptExecutionRunStep step |
45+ this = trimQuotes ( getUnnormalizedLocalScriptPath ( step ) )
46+ )
47+ or
48+ exists ( LocalActionUsesStep step | this = step .getCallee ( ) )
49+ }
50+ }
51+
52+ private string getNormalizedActionsCheckoutPath ( PRHeadCheckoutStep checkout ) {
53+ exists ( ActionsCheckoutPathInput checkoutPath , string normalized |
54+ checkout instanceof UsesStep and
55+ checkout .( UsesStep ) .getCallee ( ) = "actions/checkout" and
56+ checkoutPath = trimQuotes ( checkout .( UsesStep ) .getArgument ( "path" ) ) and
57+ not checkoutPath .regexpMatch ( ".*\\$\\{\\{.*" ) and
58+ normalized = checkoutPath .getNormalizedPath ( ) and
59+ not normalized .matches ( "/%" ) and
60+ normalized != ".." and
61+ not normalized .matches ( "../%" ) and
62+ if normalized = "."
63+ then result = "GITHUB_WORKSPACE"
64+ else result = "GITHUB_WORKSPACE/" + normalized
65+ )
66+ }
67+
68+ bindingset [ path]
69+ private string getNormalizedExecutionPath ( string path ) {
70+ exists ( ExecutionPathInput executionPath , string normalized |
71+ executionPath = trimQuotes ( path ) and
72+ not executionPath .regexpMatch ( ".*\\$\\{\\{.*" ) and
73+ normalized = executionPath .getNormalizedPath ( ) and
74+ not normalized .matches ( "/%" ) and
75+ normalized != ".." and
76+ not normalized .matches ( "../%" ) and
77+ if normalized = "."
78+ then result = "GITHUB_WORKSPACE"
79+ else (
80+ normalized .regexpMatch ( "^[^$/~].*" ) and
81+ result = "GITHUB_WORKSPACE/" + normalized
82+ )
83+ )
84+ }
85+
86+ bindingset [ checkout, rawPath, path]
87+ private predicate checkoutContainsPath ( PRHeadCheckoutStep checkout , string rawPath , string path ) {
88+ exists ( string root , string candidate |
89+ root = getNormalizedActionsCheckoutPath ( checkout ) and
90+ candidate = getNormalizedExecutionPath ( rawPath ) and
91+ // Canonicalize both paths so dot segments cannot enter or escape the checkout while still
92+ // passing a lexical prefix check.
93+ ( candidate = root or candidate .indexOf ( root + "/" ) = 0 )
94+ )
95+ or
96+ not exists ( getNormalizedActionsCheckoutPath ( checkout ) ) and
97+ isSubpath ( path , checkout .getPath ( ) )
98+ }
99+
100+ private predicate checkoutUsesWorkspaceRoot ( PRHeadCheckoutStep checkout ) {
101+ getNormalizedActionsCheckoutPath ( checkout ) = "GITHUB_WORKSPACE"
102+ or
103+ not exists ( getNormalizedActionsCheckoutPath ( checkout ) ) and
104+ checkout .getPath ( ) = "GITHUB_WORKSPACE/"
105+ }
106+
23107from PRHeadCheckoutStep checkout , PoisonableStep poisonable , Event event
24108where
25109 // the checkout is followed by a known poisonable step
29113 (
30114 // Check if the poisonable step is a local script execution step
31115 // and the path of the command or script matches the path of the downloaded artifact
32- isSubpath ( poisonable .( LocalScriptExecutionRunStep ) .getPath ( ) , checkout .getPath ( ) )
116+ checkoutContainsPath ( checkout ,
117+ getUnnormalizedLocalScriptPath ( poisonable .( LocalScriptExecutionRunStep ) ) ,
118+ poisonable .( LocalScriptExecutionRunStep ) .getPath ( ) )
33119 or
34120 // Checking the path for non local script execution steps is very difficult
35121 not poisonable instanceof LocalScriptExecutionRunStep
@@ -40,9 +126,10 @@ where
40126 poisonable instanceof UsesStep and
41127 (
42128 not poisonable instanceof LocalActionUsesStep and
43- checkout . getPath ( ) = "GITHUB_WORKSPACE/"
129+ checkoutUsesWorkspaceRoot ( checkout )
44130 or
45- isSubpath ( poisonable .( LocalActionUsesStep ) .getPath ( ) , checkout .getPath ( ) )
131+ checkoutContainsPath ( checkout , poisonable .( LocalActionUsesStep ) .getCallee ( ) ,
132+ poisonable .( LocalActionUsesStep ) .getPath ( ) )
46133 )
47134 ) and
48135 // the checkout occurs in a privileged context
0 commit comments