Skip to content

Commit a3ccc2e

Browse files
author
Alvaro Muñoz
authored
Merge pull request #30 from GitHubSecurityLab/untrusted_co
Improve UntrustedCheckout query
2 parents aa62603 + 778d897 commit a3ccc2e

32 files changed

+822
-155
lines changed

ql/lib/codeql/actions/Ast.qll

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
private import codeql.actions.ast.internal.Ast
22
private import codeql.Locations
33

4+
module Utils {
5+
bindingset[expr]
6+
string normalizeExpr(string expr) {
7+
result =
8+
expr.regexpReplaceAll("\\['([a-zA-Z0-9_\\*\\-]+)'\\]", ".$1")
9+
.regexpReplaceAll("\\[\"([a-zA-Z0-9_\\*\\-]+)\"\\]", ".$1")
10+
.regexpReplaceAll("\\s*\\.\\s*", ".")
11+
}
12+
}
13+
414
class AstNode instanceof AstNodeImpl {
515
AstNode getAChildNode() { result = super.getAChildNode() }
616

@@ -33,6 +43,8 @@ class Expression extends AstNode instanceof ExpressionImpl {
3343
string getExpression() { result = expression }
3444

3545
string getRawExpression() { result = rawExpression }
46+
47+
string getNormalizedExpression() { result = Utils::normalizeExpr(expression) }
3648
}
3749

3850
/** A common class for `env` in workflow, job or step. */
@@ -188,6 +200,8 @@ class Step extends AstNode instanceof StepImpl {
188200
*/
189201
class If extends AstNode instanceof IfImpl {
190202
string getCondition() { result = super.getCondition() }
203+
204+
Expression getConditionExpr() { result = super.getConditionExpr() }
191205
}
192206

193207
abstract class Uses extends AstNode instanceof UsesImpl {
@@ -212,20 +226,22 @@ class Run extends Step instanceof RunImpl {
212226
Expression getAnScriptExpr() { result = super.getAnScriptExpr() }
213227
}
214228

215-
abstract class ContextExpression extends AstNode instanceof ContextExpressionImpl {
229+
abstract class SimpleReferenceExpression extends AstNode instanceof SimpleReferenceExpressionImpl {
216230
string getFieldName() { result = super.getFieldName() }
217231

218232
AstNode getTarget() { result = super.getTarget() }
219233
}
220234

221-
class StepsExpression extends ContextExpression instanceof StepsExpressionImpl { }
235+
class StepsExpression extends SimpleReferenceExpression instanceof StepsExpressionImpl {
236+
string getStepId() { result = super.getStepId() }
237+
}
222238

223-
class NeedsExpression extends ContextExpression instanceof NeedsExpressionImpl { }
239+
class NeedsExpression extends SimpleReferenceExpression instanceof NeedsExpressionImpl { }
224240

225-
class JobsExpression extends ContextExpression instanceof JobsExpressionImpl { }
241+
class JobsExpression extends SimpleReferenceExpression instanceof JobsExpressionImpl { }
226242

227-
class InputsExpression extends ContextExpression instanceof InputsExpressionImpl { }
243+
class InputsExpression extends SimpleReferenceExpression instanceof InputsExpressionImpl { }
228244

229-
class EnvExpression extends ContextExpression instanceof EnvExpressionImpl { }
245+
class EnvExpression extends SimpleReferenceExpression instanceof EnvExpressionImpl { }
230246

231-
class MatrixExpression extends ContextExpression instanceof MatrixExpressionImpl { }
247+
class MatrixExpression extends SimpleReferenceExpression instanceof MatrixExpressionImpl { }

ql/lib/codeql/actions/ast/internal/Ast.qll

Lines changed: 69 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
private import codeql.actions.ast.internal.Yaml
22
private import codeql.Locations
3+
private import codeql.actions.Ast::Utils as Utils
34

45
/**
56
* Gets the length of each line in the StringValue .
@@ -18,24 +19,19 @@ int partialLineLengthSum(string text, int i) {
1819
result = sum(int j, int length | j in [0 .. i] and length = lineLength(text, j) | length)
1920
}
2021

21-
/**
22-
* Holds if `${{ e }}` is a GitHub Actions expression evaluated within this YAML string.
23-
* See https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions.
24-
* Only finds simple expressions like `${{ github.event.comment.body }}`, where the expression contains only alphanumeric characters, underscores, dots, or dashes.
25-
* Does not identify more complicated expressions like `${{ fromJSON(env.time) }}`, or ${{ format('{{Hello {0}!}}', github.event.head_commit.author.name) }}
26-
*/
27-
string getASimpleReferenceExpression(YamlString s, int offset) {
22+
string getADelimitedExpression(YamlString s, int offset) {
2823
// We use `regexpFind` to obtain *all* matches of `${{...}}`,
2924
// not just the last (greedy match) or first (reluctant match).
3025
result =
3126
s.getValue()
32-
.regexpFind("\\$\\{\\{\\s*[A-Za-z0-9_\\[\\]\\*\\(\\)\\.\\-]+\\s*\\}\\}", _, offset)
33-
.regexpCapture("(\\$\\{\\{\\s*[A-Za-z0-9_\\[\\]\\*\\((\\)\\.\\-]+\\s*\\}\\})", 1)
27+
.regexpFind("\\$\\{\\{(?:[^}]|}(?!}))*\\}\\}", _, offset)
28+
.regexpCapture("(\\$\\{\\{(?:[^}]|}(?!}))*\\}\\})", 1)
29+
.trim()
3430
}
3531

3632
private newtype TAstNode =
3733
TExpressionNode(YamlNode key, YamlScalar value, string raw, int exprOffset) {
38-
raw = getASimpleReferenceExpression(value, exprOffset) and
34+
raw = getADelimitedExpression(value, exprOffset) and
3935
exists(YamlMapping m |
4036
(
4137
exists(int i | value = m.getValueNode(i) and key = m.getKeyNode(i))
@@ -45,6 +41,14 @@ private newtype TAstNode =
4541
)
4642
)
4743
)
44+
or
45+
// `if`'s conditions do not need to be delimted with ${{}}
46+
exists(YamlMapping m |
47+
m.maps(key, value) and
48+
key.(YamlScalar).getValue() = ["if"] and
49+
value.getValue() = raw and
50+
exprOffset = 1
51+
)
4852
} or
4953
TCompositeAction(YamlMapping n) {
5054
n instanceof YamlDocument and
@@ -123,7 +127,7 @@ class ScalarValueImpl extends AstNodeImpl, TScalarValueNode {
123127

124128
override Location getLocation() { result = value.getLocation() }
125129

126-
override YamlNode getNode() { result = value }
130+
override YamlScalar getNode() { result = value }
127131
}
128132

129133
class ExpressionImpl extends AstNodeImpl, TExpressionNode {
@@ -135,15 +139,16 @@ class ExpressionImpl extends AstNodeImpl, TExpressionNode {
135139

136140
ExpressionImpl() {
137141
this = TExpressionNode(key, value, rawExpression, exprOffset - 1) and
138-
expression =
139-
rawExpression.regexpCapture("\\$\\{\\{\\s*([A-Za-z0-9_\\[\\]\\*\\((\\)\\.\\-]+)\\s*\\}\\}", 1)
142+
if rawExpression.trim().regexpMatch("\\$\\{\\{.*\\}\\}")
143+
then expression = rawExpression.trim().regexpCapture("\\$\\{\\{\\s*(.*)\\s*\\}\\}", 1).trim()
144+
else expression = rawExpression.trim()
140145
}
141146

142147
override string toString() { result = expression }
143148

144149
override AstNodeImpl getAChildNode() { none() }
145150

146-
override AstNodeImpl getParentNode() { result.getNode() = value }
151+
override ScalarValueImpl getParentNode() { result.getNode() = value }
147152

148153
override string getAPrimaryQlClass() { result = "ExpressionNode" }
149154

@@ -638,6 +643,9 @@ class IfImpl extends AstNodeImpl, TIfNode {
638643

639644
/** Gets the condition that must be satisfied for this job to run. */
640645
string getCondition() { result = n.(YamlScalar).getValue() }
646+
647+
/** Gets the condition that must be satisfied for this job to run. */
648+
ExpressionImpl getConditionExpr() { result.getParentNode().getNode() = n }
641649
}
642650

643651
class EnvImpl extends AstNodeImpl, TEnvNode {
@@ -776,11 +784,29 @@ class RunImpl extends StepImpl {
776784
}
777785
}
778786

787+
/**
788+
* Holds if `${{ e }}` is a GitHub Actions expression evaluated within this YAML string.
789+
* See https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions.
790+
* Only finds simple expressions like `${{ github.event.comment.body }}`, where the expression contains only alphanumeric characters, underscores, dots, or dashes.
791+
* Does not identify more complicated expressions like `${{ fromJSON(env.time) }}`, or ${{ format('{{Hello {0}!}}', github.event.head_commit.author.name) }}
792+
*/
793+
bindingset[s]
794+
string getASimpleReferenceExpression(string s, int offset) {
795+
// We use `regexpFind` to obtain *all* matches of `${{...}}`,
796+
// not just the last (greedy match) or first (reluctant match).
797+
result =
798+
s.trim()
799+
.regexpFind("[A-Za-z0-9'\"_\\[\\]\\*\\(\\)\\.\\-]+", _, offset)
800+
.regexpCapture("([A-Za-z0-9'\"_\\[\\]\\*\\(\\)\\.\\-]+)", 1)
801+
}
802+
779803
/**
780804
* A ${{}} expression accessing a context variable such as steps, needs, jobs, env, inputs, or matrix.
781805
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
782806
*/
783-
abstract class ContextExpressionImpl extends ExpressionImpl {
807+
abstract class SimpleReferenceExpressionImpl extends ExpressionImpl {
808+
SimpleReferenceExpressionImpl() { exists(getASimpleReferenceExpression(expression, _)) }
809+
784810
abstract string getFieldName();
785811

786812
abstract AstNodeImpl getTarget();
@@ -816,44 +842,49 @@ private string wrapRegexp(string regex) {
816842
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
817843
* e.g. `${{ steps.changed-files.outputs.all_changed_files }}`
818844
*/
819-
class StepsExpressionImpl extends ContextExpressionImpl {
845+
class StepsExpressionImpl extends SimpleReferenceExpressionImpl {
820846
string stepId;
821847
string fieldName;
822848

823849
StepsExpressionImpl() {
824-
expression.regexpMatch(stepsCtxRegex()) and
825-
stepId = expression.regexpCapture(stepsCtxRegex(), 1) and
826-
fieldName = expression.regexpCapture(stepsCtxRegex(), 2)
850+
Utils::normalizeExpr(expression).regexpMatch(stepsCtxRegex()) and
851+
stepId = Utils::normalizeExpr(expression).regexpCapture(stepsCtxRegex(), 1) and
852+
fieldName = Utils::normalizeExpr(expression).regexpCapture(stepsCtxRegex(), 2)
827853
}
828854

829855
override string getFieldName() { result = fieldName }
830856

831857
override AstNodeImpl getTarget() {
832-
this.getLocation().getFile() = result.getLocation().getFile() and
858+
this.getEnclosingJob() = result.getEnclosingJob() and
833859
result.(StepImpl).getId() = stepId
834860
}
861+
862+
string getStepId() { result = stepId }
835863
}
836864

837865
/**
838866
* Holds for an expression accesing the `needs` context.
839867
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
840868
* e.g. `${{ needs.job1.outputs.foo}}`
841869
*/
842-
class NeedsExpressionImpl extends ContextExpressionImpl {
870+
class NeedsExpressionImpl extends SimpleReferenceExpressionImpl {
843871
JobImpl neededJob;
844872
string fieldName;
845873

846874
NeedsExpressionImpl() {
847-
expression.regexpMatch(needsCtxRegex()) and
848-
fieldName = expression.regexpCapture(needsCtxRegex(), 2) and
849-
neededJob.getId() = expression.regexpCapture(needsCtxRegex(), 1) and
875+
Utils::normalizeExpr(expression).regexpMatch(needsCtxRegex()) and
876+
fieldName = Utils::normalizeExpr(expression).regexpCapture(needsCtxRegex(), 2) and
877+
neededJob.getId() = Utils::normalizeExpr(expression).regexpCapture(needsCtxRegex(), 1) and
850878
neededJob.getLocation().getFile() = this.getLocation().getFile()
851879
}
852880

853881
override string getFieldName() { result = fieldName }
854882

855883
override AstNodeImpl getTarget() {
856-
this.getEnclosingJob().getANeededJob() = neededJob and
884+
(
885+
this.getEnclosingJob().getANeededJob() = neededJob or
886+
this.getEnclosingJob() = neededJob
887+
) and
857888
(
858889
// regular jobs
859890
neededJob.getOutputs() = result
@@ -869,14 +900,14 @@ class NeedsExpressionImpl extends ContextExpressionImpl {
869900
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
870901
* e.g. `${{ jobs.job1.outputs.foo}}` (within reusable workflows)
871902
*/
872-
class JobsExpressionImpl extends ContextExpressionImpl {
903+
class JobsExpressionImpl extends SimpleReferenceExpressionImpl {
873904
string jobId;
874905
string fieldName;
875906

876907
JobsExpressionImpl() {
877-
expression.regexpMatch(jobsCtxRegex()) and
878-
jobId = expression.regexpCapture(jobsCtxRegex(), 1) and
879-
fieldName = expression.regexpCapture(jobsCtxRegex(), 2)
908+
Utils::normalizeExpr(expression).regexpMatch(jobsCtxRegex()) and
909+
jobId = Utils::normalizeExpr(expression).regexpCapture(jobsCtxRegex(), 1) and
910+
fieldName = Utils::normalizeExpr(expression).regexpCapture(jobsCtxRegex(), 2)
880911
}
881912

882913
override string getFieldName() { result = fieldName }
@@ -895,12 +926,12 @@ class JobsExpressionImpl extends ContextExpressionImpl {
895926
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
896927
* e.g. `${{ inputs.foo }}`
897928
*/
898-
class InputsExpressionImpl extends ContextExpressionImpl {
929+
class InputsExpressionImpl extends SimpleReferenceExpressionImpl {
899930
string fieldName;
900931

901932
InputsExpressionImpl() {
902-
expression.regexpMatch(inputsCtxRegex()) and
903-
fieldName = expression.regexpCapture(inputsCtxRegex(), 1)
933+
Utils::normalizeExpr(expression).regexpMatch(inputsCtxRegex()) and
934+
fieldName = Utils::normalizeExpr(expression).regexpCapture(inputsCtxRegex(), 1)
904935
}
905936

906937
override string getFieldName() { result = fieldName }
@@ -920,12 +951,12 @@ class InputsExpressionImpl extends ContextExpressionImpl {
920951
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
921952
* e.g. `${{ env.foo }}`
922953
*/
923-
class EnvExpressionImpl extends ContextExpressionImpl {
954+
class EnvExpressionImpl extends SimpleReferenceExpressionImpl {
924955
string fieldName;
925956

926957
EnvExpressionImpl() {
927-
expression.regexpMatch(envCtxRegex()) and
928-
fieldName = expression.regexpCapture(envCtxRegex(), 1)
958+
Utils::normalizeExpr(expression).regexpMatch(envCtxRegex()) and
959+
fieldName = Utils::normalizeExpr(expression).regexpCapture(envCtxRegex(), 1)
929960
}
930961

931962
override string getFieldName() { result = fieldName }
@@ -943,12 +974,12 @@ class EnvExpressionImpl extends ContextExpressionImpl {
943974
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
944975
* e.g. `${{ matrix.foo }}`
945976
*/
946-
class MatrixExpressionImpl extends ContextExpressionImpl {
977+
class MatrixExpressionImpl extends SimpleReferenceExpressionImpl {
947978
string fieldName;
948979

949980
MatrixExpressionImpl() {
950-
expression.regexpMatch(matrixCtxRegex()) and
951-
fieldName = expression.regexpCapture(matrixCtxRegex(), 1)
981+
Utils::normalizeExpr(expression).regexpMatch(matrixCtxRegex()) and
982+
fieldName = Utils::normalizeExpr(expression).regexpCapture(matrixCtxRegex(), 1)
952983
}
953984

954985
override string getFieldName() { result = fieldName }

ql/lib/codeql/actions/dataflow/ExternalFlow.qll

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
private import internal.ExternalFlowExtensions as Extensions
2-
import codeql.actions.DataFlow
3-
import actions
2+
private import codeql.actions.DataFlow
3+
private import actions
44

55
/**
66
* MaD sources

0 commit comments

Comments
 (0)