Skip to content

Commit 4bef451

Browse files
authored
Merge pull request #9021 from erik-krogh/actions
JS: promote `js/actions/injection` out of experimental
2 parents 7cd51d6 + fef4455 commit 4bef451

File tree

14 files changed

+379
-360
lines changed

14 files changed

+379
-360
lines changed
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
/**
2+
* Libraries for modeling GitHub Actions workflow files written in YAML.
3+
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
4+
*/
5+
6+
import javascript
7+
8+
/**
9+
* Libraries for modeling GitHub Actions workflow files written in YAML.
10+
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
11+
*/
12+
module Actions {
13+
/** A YAML node in a GitHub Actions workflow file. */
14+
private class Node extends YAMLNode {
15+
Node() {
16+
this.getLocation()
17+
.getFile()
18+
.getRelativePath()
19+
.regexpMatch("(^|.*/)\\.github/workflows/.*\\.yml$")
20+
}
21+
}
22+
23+
/**
24+
* An Actions workflow. This is a mapping at the top level of an Actions YAML workflow file.
25+
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
26+
*/
27+
class Workflow extends Node, YAMLDocument, YAMLMapping {
28+
/** Gets the `jobs` mapping from job IDs to job definitions in this workflow. */
29+
YAMLMapping getJobs() { result = this.lookup("jobs") }
30+
31+
/** Gets the name of the workflow file. */
32+
string getFileName() { result = this.getFile().getBaseName() }
33+
34+
/** Gets the `on:` in this workflow. */
35+
On getOn() { result = this.lookup("on") }
36+
37+
/** Gets the job within this workflow with the given job ID. */
38+
Job getJob(string jobId) { result.getWorkflow() = this and result.getId() = jobId }
39+
}
40+
41+
/**
42+
* An Actions On trigger within a workflow.
43+
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#on.
44+
*/
45+
class On extends YAMLNode, YAMLMappingLikeNode {
46+
Workflow workflow;
47+
48+
On() { workflow.lookup("on") = this }
49+
50+
/** Gets the workflow that this trigger is in. */
51+
Workflow getWorkflow() { result = workflow }
52+
}
53+
54+
/**
55+
* An Actions job within a workflow.
56+
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobs.
57+
*/
58+
class Job extends YAMLNode, YAMLMapping {
59+
string jobId;
60+
Workflow workflow;
61+
62+
Job() { this = workflow.getJobs().lookup(jobId) }
63+
64+
/**
65+
* Gets the ID of this job, as a string.
66+
* This is the job's key within the `jobs` mapping.
67+
*/
68+
string getId() { result = jobId }
69+
70+
/**
71+
* Gets the ID of this job, as a YAML scalar node.
72+
* This is the job's key within the `jobs` mapping.
73+
*/
74+
YAMLString getIdNode() { workflow.getJobs().maps(result, this) }
75+
76+
/** Gets the human-readable name of this job, if any, as a string. */
77+
string getName() { result = this.getNameNode().getValue() }
78+
79+
/** Gets the human-readable name of this job, if any, as a YAML scalar node. */
80+
YAMLString getNameNode() { result = this.lookup("name") }
81+
82+
/** Gets the step at the given index within this job. */
83+
Step getStep(int index) { result.getJob() = this and result.getIndex() = index }
84+
85+
/** Gets the sequence of `steps` within this job. */
86+
YAMLSequence getSteps() { result = this.lookup("steps") }
87+
88+
/** Gets the workflow this job belongs to. */
89+
Workflow getWorkflow() { result = workflow }
90+
91+
/** Gets the value of the `if` field in this job, if any. */
92+
JobIf getIf() { result.getJob() = this }
93+
}
94+
95+
/**
96+
* An `if` within a job.
97+
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idif.
98+
*/
99+
class JobIf extends YAMLNode, YAMLScalar {
100+
Job job;
101+
102+
JobIf() { job.lookup("if") = this }
103+
104+
/** Gets the step this field belongs to. */
105+
Job getJob() { result = job }
106+
}
107+
108+
/**
109+
* A step within an Actions job.
110+
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idsteps.
111+
*/
112+
class Step extends YAMLNode, YAMLMapping {
113+
int index;
114+
Job job;
115+
116+
Step() { this = job.getSteps().getElement(index) }
117+
118+
/** Gets the 0-based position of this step within the sequence of `steps`. */
119+
int getIndex() { result = index }
120+
121+
/** Gets the job this step belongs to. */
122+
Job getJob() { result = job }
123+
124+
/** Gets the value of the `uses` field in this step, if any. */
125+
Uses getUses() { result.getStep() = this }
126+
127+
/** Gets the value of the `run` field in this step, if any. */
128+
Run getRun() { result.getStep() = this }
129+
130+
/** Gets the value of the `if` field in this step, if any. */
131+
StepIf getIf() { result.getStep() = this }
132+
}
133+
134+
/**
135+
* An `if` within a step.
136+
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsif.
137+
*/
138+
class StepIf extends YAMLNode, YAMLScalar {
139+
Step step;
140+
141+
StepIf() { step.lookup("if") = this }
142+
143+
/** Gets the step this field belongs to. */
144+
Step getStep() { result = step }
145+
}
146+
147+
/**
148+
* Gets a regular expression that parses an `owner/repo@version` reference within a `uses` field in an Actions job step.
149+
* The capture groups are:
150+
* 1: The owner of the repository where the Action comes from, e.g. `actions` in `actions/checkout@v2`
151+
* 2: The name of the repository where the Action comes from, e.g. `checkout` in `actions/checkout@v2`.
152+
* 3: The version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`.
153+
*/
154+
private string usesParser() { result = "([^/]+)/([^/@]+)@(.+)" }
155+
156+
/**
157+
* A `uses` field within an Actions job step, which references an action as a reusable unit of code.
158+
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsuses.
159+
*
160+
* For example:
161+
* ```
162+
* uses: actions/checkout@v2
163+
* ```
164+
*
165+
* Does not handle local repository references, e.g. `.github/actions/action-name`.
166+
*/
167+
class Uses extends YAMLNode, YAMLScalar {
168+
Step step;
169+
170+
Uses() { step.lookup("uses") = this }
171+
172+
/** Gets the step this field belongs to. */
173+
Step getStep() { result = step }
174+
175+
/** Gets the owner and name of the repository where the Action comes from, e.g. `actions/checkout` in `actions/checkout@v2`. */
176+
string getGitHubRepository() {
177+
result =
178+
this.getValue().regexpCapture(usesParser(), 1) + "/" +
179+
this.getValue().regexpCapture(usesParser(), 2)
180+
}
181+
182+
/** Gets the version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`. */
183+
string getVersion() { result = this.getValue().regexpCapture(usesParser(), 3) }
184+
}
185+
186+
/**
187+
* A `with` field within an Actions job step, which references an action as a reusable unit of code.
188+
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepswith.
189+
*
190+
* For example:
191+
* ```
192+
* with:
193+
* arg1: 1
194+
* arg2: abc
195+
* ```
196+
*/
197+
class With extends YAMLNode, YAMLMapping {
198+
Step step;
199+
200+
With() { step.lookup("with") = this }
201+
202+
/** Gets the step this field belongs to. */
203+
Step getStep() { result = step }
204+
}
205+
206+
/**
207+
* A `ref:` field within an Actions `with:` specific to `actions/checkout` action.
208+
*
209+
* For example:
210+
* ```
211+
* uses: actions/checkout@v2
212+
* with:
213+
* ref: ${{ github.event.pull_request.head.sha }}
214+
* ```
215+
*/
216+
class Ref extends YAMLNode, YAMLString {
217+
With with;
218+
219+
Ref() { with.lookup("ref") = this }
220+
221+
/** Gets the `with` field this field belongs to. */
222+
With getWith() { result = with }
223+
}
224+
225+
/**
226+
* A `run` field within an Actions job step, which runs command-line programs using an operating system shell.
227+
* See https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun.
228+
*/
229+
class Run extends YAMLNode, YAMLString {
230+
Step step;
231+
232+
Run() { step.lookup("run") = this }
233+
234+
/** Gets the step that executes this `run` command. */
235+
Step getStep() { result = step }
236+
237+
/**
238+
* Holds if `${{ e }}` is a GitHub Actions expression evaluated within this `run` command.
239+
* See https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions.
240+
* Only finds simple expressions like `${{ github.event.comment.body }}`, where the expression contains only alphanumeric characters, underscores, dots, or dashes.
241+
* Does not identify more complicated expressions like `${{ fromJSON(env.time) }}`, or ${{ format('{{Hello {0}!}}', github.event.head_commit.author.name) }}
242+
*/
243+
string getASimpleReferenceExpression() {
244+
// We use `regexpFind` to obtain *all* matches of `${{...}}`,
245+
// not just the last (greedy match) or first (reluctant match).
246+
result =
247+
this.getValue()
248+
.regexpFind("\\$\\{\\{\\s*[A-Za-z0-9_\\.\\-]+\\s*\\}\\}", _, _)
249+
.regexpCapture("\\$\\{\\{\\s*([A-Za-z0-9_\\.\\-]+)\\s*\\}\\}", 1)
250+
}
251+
}
252+
}

javascript/ql/lib/semmle/javascript/YAML.qll

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,3 +441,74 @@ class YAMLParseError extends @yaml_error, Error {
441441

442442
override string toString() { result = this.getMessage() }
443443
}
444+
445+
/**
446+
* A YAML node that may contain sub-nodes that can be identified by a name.
447+
* I.e. a mapping, sequence, or scalar.
448+
*
449+
* Is used in e.g. GithHub Actions, which is quite flexible in parsing YAML.
450+
*
451+
* For example:
452+
* ```
453+
* on: pull_request
454+
* ```
455+
* and
456+
* ```
457+
* on: [pull_request]
458+
* ```
459+
* and
460+
* ```
461+
* on:
462+
* pull_request:
463+
* ```
464+
*
465+
* are equivalent.
466+
*/
467+
class YAMLMappingLikeNode extends YAMLNode {
468+
YAMLMappingLikeNode() {
469+
this instanceof YAMLMapping
470+
or
471+
this instanceof YAMLSequence
472+
or
473+
this instanceof YAMLScalar
474+
}
475+
476+
/** Gets sub-name identified by `name`. */
477+
YAMLNode getNode(string name) {
478+
exists(YAMLMapping mapping |
479+
mapping = this and
480+
result = mapping.lookup(name)
481+
)
482+
or
483+
exists(YAMLSequence sequence, YAMLNode node |
484+
sequence = this and
485+
sequence.getAChildNode() = node and
486+
node.eval().toString() = name and
487+
result = node
488+
)
489+
or
490+
exists(YAMLScalar scalar |
491+
scalar = this and
492+
scalar.getValue() = name and
493+
result = scalar
494+
)
495+
}
496+
497+
/** Gets the number of elements in this mapping or sequence. */
498+
int getElementCount() {
499+
exists(YAMLMapping mapping |
500+
mapping = this and
501+
result = mapping.getNumChild() / 2
502+
)
503+
or
504+
exists(YAMLSequence sequence |
505+
sequence = this and
506+
result = sequence.getNumChild()
507+
)
508+
or
509+
exists(YAMLScalar scalar |
510+
scalar = this and
511+
result = 1
512+
)
513+
}
514+
}

javascript/ql/src/experimental/Security/CWE-094/ExpressionInjection.qhelp renamed to javascript/ql/src/Security/CWE-094/ExpressionInjection.qhelp

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,47 @@
22
"-//Semmle//qhelp//EN"
33
"qhelp.dtd">
44
<qhelp>
5-
65
<overview>
7-
86
<p>
9-
107
Using user-controlled input in GitHub Actions may lead to
118
code injection in contexts like <i>run:</i> or <i>script:</i>.
12-
139
</p>
14-
10+
<p>
11+
Code injection in GitHub Actions may allow an attacker to
12+
exfiltrate the temporary GitHub repository authorization token.
13+
The token might have write access to the repository, allowing an attacker
14+
to use the token to make changes to the repository.
15+
</p>
1516
</overview>
1617

1718
<recommendation>
18-
1919
<p>
20-
2120
The best practice to avoid code injection vulnerabilities
2221
in GitHub workflows is to set the untrusted input value of the expression
2322
to an intermediate environment variable.
24-
2523
</p>
26-
24+
<p>
25+
It is also recommended to limit the permissions of any tokens used
26+
by a workflow such as the the GITHUB_TOKEN.
27+
</p>
2728
</recommendation>
2829

2930
<example>
30-
3131
<p>
32-
3332
The following example lets a user inject an arbitrary shell command:
34-
3533
</p>
36-
3734
<sample src="examples/comment_issue_bad.yml" />
3835

3936
<p>
40-
4137
The following example uses shell syntax to read
4238
the environment variable and will prevent the attack:
43-
4439
</p>
45-
4640
<sample src="examples/comment_issue_good.yml" />
47-
4841
</example>
4942

5043
<references>
5144
<li>GitHub Security Lab Research: <a href="https://securitylab.github.com/research/github-actions-untrusted-input">Keeping your GitHub Actions and workflows secure: Untrusted input</a>.</li>
45+
<li>GitHub Docs: <a href="https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions">Security hardening for GitHub Actions</a>.</li>
46+
<li>GitHub Docs: <a href="https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token">Permissions for the GITHUB_TOKEN</a>.</li>
5247
</references>
53-
5448
</qhelp>

0 commit comments

Comments
 (0)