Skip to content

Commit 20e8ee8

Browse files
authored
Merge pull request github#12748 from JarLob/yi
JS: Add more sources, more unit tests, fixes to the GitHub Actions injection query
2 parents cc6da7e + 891a94c commit 20e8ee8

File tree

22 files changed

+593
-115
lines changed

22 files changed

+593
-115
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Improved the queries for injection vulnerabilities in GitHub Actions workflows (`js/actions/command-injection` and `js/actions/pull-request-target`) and the associated library `semmle.javascript.Actions`. These now support steps defined in composite actions, in addition to steps defined in Actions workflow files. It supports more potentially untrusted input values. Additionally to the shell injections it now also detects injections in `actions/github-script`. It also detects simple injections from user controlled `${{ env.name }}`. Additionally to the `yml` extension now it also supports workflows with the `yaml` extension.

javascript/ql/lib/semmle/javascript/Actions.qll

Lines changed: 127 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,70 @@ import javascript
1010
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
1111
*/
1212
module Actions {
13-
/** A YAML node in a GitHub Actions workflow file. */
13+
/** A YAML node in a GitHub Actions workflow or a custom composite action file. */
1414
private class Node extends YamlNode {
1515
Node() {
16-
this.getLocation()
17-
.getFile()
18-
.getRelativePath()
19-
.regexpMatch("(^|.*/)\\.github/workflows/.*\\.yml$")
16+
exists(File f |
17+
f = this.getLocation().getFile() and
18+
(
19+
f.getRelativePath().regexpMatch("(^|.*/)\\.github/workflows/.*\\.ya?ml$")
20+
or
21+
f.getBaseName() = ["action.yml", "action.yaml"]
22+
)
23+
)
2024
}
2125
}
2226

27+
/**
28+
* A custom composite action. This is a mapping at the top level of an Actions YAML action file.
29+
* See https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions.
30+
*/
31+
class CompositeAction extends Node, YamlDocument, YamlMapping {
32+
CompositeAction() {
33+
this.getFile().getBaseName() = ["action.yml", "action.yaml"] and
34+
this.lookup("runs").(YamlMapping).lookup("using").(YamlScalar).getValue() = "composite"
35+
}
36+
37+
/** Gets the `runs` mapping. */
38+
Runs getRuns() { result = this.lookup("runs") }
39+
}
40+
41+
/**
42+
* An `runs` mapping in a custom composite action YAML.
43+
* See https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs
44+
*/
45+
class Runs extends StepsContainer {
46+
CompositeAction action;
47+
48+
Runs() { action.lookup("runs") = this }
49+
50+
/** Gets the action that this `runs` mapping is in. */
51+
CompositeAction getAction() { result = action }
52+
53+
/** Gets the `using` mapping. */
54+
Using getUsing() { result = this.lookup("using") }
55+
}
56+
57+
/**
58+
* The parent class of the class that can contain `steps` mappings. (`Job` or `Runs` currently.)
59+
*/
60+
abstract class StepsContainer extends YamlNode, YamlMapping {
61+
/** Gets the sequence of `steps` within this YAML node. */
62+
YamlSequence getSteps() { result = this.lookup("steps") }
63+
}
64+
65+
/**
66+
* A `using` mapping in a custom composite action YAML.
67+
*/
68+
class Using extends YamlNode, YamlScalar {
69+
Runs runs;
70+
71+
Using() { runs.lookup("using") = this }
72+
73+
/** Gets the `runs` mapping that this `using` mapping is in. */
74+
Runs getRuns() { result = runs }
75+
}
76+
2377
/**
2478
* An Actions workflow. This is a mapping at the top level of an Actions YAML workflow file.
2579
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
@@ -28,6 +82,9 @@ module Actions {
2882
/** Gets the `jobs` mapping from job IDs to job definitions in this workflow. */
2983
YamlMapping getJobs() { result = this.lookup("jobs") }
3084

85+
/** Gets the 'global' `env` mapping in this workflow. */
86+
WorkflowEnv getEnv() { result = this.lookup("env") }
87+
3188
/** Gets the name of the workflow. */
3289
string getName() { result = this.lookup("name").(YamlString).getValue() }
3390

@@ -54,11 +111,44 @@ module Actions {
54111
Workflow getWorkflow() { result = workflow }
55112
}
56113

114+
/** A common class for `env` in workflow, job or step. */
115+
abstract class Env extends YamlNode, YamlMapping { }
116+
117+
/** A workflow level `env` mapping. */
118+
class WorkflowEnv extends Env {
119+
Workflow workflow;
120+
121+
WorkflowEnv() { workflow.lookup("env") = this }
122+
123+
/** Gets the workflow this field belongs to. */
124+
Workflow getWorkflow() { result = workflow }
125+
}
126+
127+
/** A job level `env` mapping. */
128+
class JobEnv extends Env {
129+
Job job;
130+
131+
JobEnv() { job.lookup("env") = this }
132+
133+
/** Gets the job this field belongs to. */
134+
Job getJob() { result = job }
135+
}
136+
137+
/** A step level `env` mapping. */
138+
class StepEnv extends Env {
139+
Step step;
140+
141+
StepEnv() { step.lookup("env") = this }
142+
143+
/** Gets the step this field belongs to. */
144+
Step getStep() { result = step }
145+
}
146+
57147
/**
58148
* An Actions job within a workflow.
59149
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobs.
60150
*/
61-
class Job extends YamlNode, YamlMapping {
151+
class Job extends StepsContainer {
62152
string jobId;
63153
Workflow workflow;
64154

@@ -85,8 +175,8 @@ module Actions {
85175
/** Gets the step at the given index within this job. */
86176
Step getStep(int index) { result.getJob() = this and result.getIndex() = index }
87177

88-
/** Gets the sequence of `steps` within this job. */
89-
YamlSequence getSteps() { result = this.lookup("steps") }
178+
/** Gets the `env` mapping in this job. */
179+
JobEnv getEnv() { result = this.lookup("env") }
90180

91181
/** Gets the workflow this job belongs to. */
92182
Workflow getWorkflow() { result = workflow }
@@ -130,15 +220,18 @@ module Actions {
130220
*/
131221
class Step extends YamlNode, YamlMapping {
132222
int index;
133-
Job job;
223+
StepsContainer parent;
134224

135-
Step() { this = job.getSteps().getElement(index) }
225+
Step() { this = parent.getSteps().getElement(index) }
136226

137227
/** Gets the 0-based position of this step within the sequence of `steps`. */
138228
int getIndex() { result = index }
139229

140-
/** Gets the job this step belongs to. */
141-
Job getJob() { result = job }
230+
/** Gets the `job` this step belongs to, if the step belongs to a `job` in a workflow. Has no result if the step belongs to `runs` in a custom composite action. */
231+
Job getJob() { result = parent }
232+
233+
/** Gets the `runs` this step belongs to, if the step belongs to a `runs` in a custom composite action. Has no result if the step belongs to a `job` in a workflow. */
234+
Runs getRuns() { result = parent }
142235

143236
/** Gets the value of the `uses` field in this step, if any. */
144237
Uses getUses() { result.getStep() = this }
@@ -149,6 +242,9 @@ module Actions {
149242
/** Gets the value of the `if` field in this step, if any. */
150243
StepIf getIf() { result.getStep() = this }
151244

245+
/** Gets the value of the `env` field in this step, if any. */
246+
StepEnv getEnv() { result = this.lookup("env") }
247+
152248
/** Gets the ID of this step, if any. */
153249
string getId() { result = this.lookup("id").(YamlString).getValue() }
154250
}
@@ -244,6 +340,25 @@ module Actions {
244340
With getWith() { result = with }
245341
}
246342

343+
/**
344+
* Holds if `${{ e }}` is a GitHub Actions expression evaluated within this YAML string.
345+
* See https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions.
346+
* Only finds simple expressions like `${{ github.event.comment.body }}`, where the expression contains only alphanumeric characters, underscores, dots, or dashes.
347+
* Does not identify more complicated expressions like `${{ fromJSON(env.time) }}`, or ${{ format('{{Hello {0}!}}', github.event.head_commit.author.name) }}
348+
*/
349+
string getASimpleReferenceExpression(YamlString node) {
350+
// We use `regexpFind` to obtain *all* matches of `${{...}}`,
351+
// not just the last (greedy match) or first (reluctant match).
352+
result =
353+
node.getValue()
354+
.regexpFind("\\$\\{\\{\\s*[A-Za-z0-9_\\[\\]\\*\\(\\)\\.\\-]+\\s*\\}\\}", _, _)
355+
.regexpCapture("\\$\\{\\{\\s*([A-Za-z0-9_\\[\\]\\*\\((\\)\\.\\-]+)\\s*\\}\\}", 1)
356+
}
357+
358+
/** Extracts the 'name' part from env.name */
359+
bindingset[name]
360+
string getEnvName(string name) { result = name.regexpCapture("env\\.([A-Za-z0-9_]+)", 1) }
361+
247362
/**
248363
* A `run` field within an Actions job step, which runs command-line programs using an operating system shell.
249364
* See https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun.
@@ -255,20 +370,5 @@ module Actions {
255370

256371
/** Gets the step that executes this `run` command. */
257372
Step getStep() { result = step }
258-
259-
/**
260-
* Holds if `${{ e }}` is a GitHub Actions expression evaluated within this `run` command.
261-
* See https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions.
262-
* Only finds simple expressions like `${{ github.event.comment.body }}`, where the expression contains only alphanumeric characters, underscores, dots, or dashes.
263-
* Does not identify more complicated expressions like `${{ fromJSON(env.time) }}`, or ${{ format('{{Hello {0}!}}', github.event.head_commit.author.name) }}
264-
*/
265-
string getASimpleReferenceExpression() {
266-
// We use `regexpFind` to obtain *all* matches of `${{...}}`,
267-
// not just the last (greedy match) or first (reluctant match).
268-
result =
269-
this.getValue()
270-
.regexpFind("\\$\\{\\{\\s*[A-Za-z0-9_\\.\\-]+\\s*\\}\\}", _, _)
271-
.regexpCapture("\\$\\{\\{\\s*([A-Za-z0-9_\\.\\-]+)\\s*\\}\\}", 1)
272-
}
273373
}
274374
}

javascript/ql/src/Security/CWE-094/ExpressionInjection.qhelp

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,20 @@
88
code injection in contexts like <i>run:</i> or <i>script:</i>.
99
</p>
1010
<p>
11-
Code injection in GitHub Actions may allow an attacker to
12-
exfiltrate the temporary GitHub repository authorization token.
11+
Code injection in GitHub Actions may allow an attacker to
12+
exfiltrate any secrets used in the workflow and
13+
the temporary GitHub repository authorization token.
1314
The token might have write access to the repository, allowing an attacker
14-
to use the token to make changes to the repository.
15+
to use the token to make changes to the repository.
1516
</p>
1617
</overview>
1718

1819
<recommendation>
1920
<p>
2021
The best practice to avoid code injection vulnerabilities
2122
in GitHub workflows is to set the untrusted input value of the expression
22-
to an intermediate environment variable.
23+
to an intermediate environment variable and then use the environment variable
24+
using the native syntax of the shell/script interpreter (that is, not <i>${{ env.VAR }}</i>).
2325
</p>
2426
<p>
2527
It is also recommended to limit the permissions of any tokens used
@@ -33,6 +35,12 @@
3335
</p>
3436
<sample src="examples/comment_issue_bad.yml" />
3537

38+
<p>
39+
The following example uses an environment variable, but
40+
<b>still allows the injection</b> because of the use of expression syntax:
41+
</p>
42+
<sample src="examples/comment_issue_bad_env.yml" />
43+
3644
<p>
3745
The following example uses shell syntax to read
3846
the environment variable and will prevent the attack:

0 commit comments

Comments
 (0)