Skip to content

Commit 5c5b9f9

Browse files
committed
Add simple taint tracking for env variables
1 parent 39ff3c7 commit 5c5b9f9

5 files changed

Lines changed: 106 additions & 12 deletions

File tree

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ module Actions {
2828
/** Gets the `jobs` mapping from job IDs to job definitions in this workflow. */
2929
YamlMapping getJobs() { result = this.lookup("jobs") }
3030

31+
/** Gets the 'global' `env` mapping in this workflow. */
32+
YamlMapping getEnv() { result = this.lookup("env") }
33+
3134
/** Gets the name of the workflow. */
3235
string getName() { result = this.lookup("name").(YamlString).getValue() }
3336

@@ -54,6 +57,54 @@ module Actions {
5457
Workflow getWorkflow() { result = workflow }
5558
}
5659

60+
/** An environment variable in 'env:' */
61+
abstract class Env extends YamlNode, YamlString {
62+
/** Gets the name of this environment variable. */
63+
abstract string getName();
64+
}
65+
66+
/** Workflow level 'global' environment variable. */
67+
class GlobalEnv extends Env {
68+
string envName;
69+
Workflow workflow;
70+
71+
GlobalEnv() { this = workflow.getEnv().lookup(envName) }
72+
73+
/** Gets the workflow this field belongs to. */
74+
Workflow getWorkflow() { result = workflow }
75+
76+
/** Gets the name of this environment variable. */
77+
override string getName() { result = envName }
78+
}
79+
80+
/** Job level environment variable. */
81+
class JobEnv extends Env {
82+
string envName;
83+
Job job;
84+
85+
JobEnv() { this = job.getEnv().lookup(envName) }
86+
87+
/** Gets the job this field belongs to. */
88+
Job getJob() { result = job }
89+
90+
/** Gets the name of this environment variable. */
91+
override string getName() { result = envName }
92+
}
93+
94+
/** Step level environment variable. */
95+
class StepEnv extends Env {
96+
string envName;
97+
Step step;
98+
99+
StepEnv() { this = step.getEnv().lookup(envName) }
100+
101+
/** Gets the step this field belongs to. */
102+
Step getStep() { result = step }
103+
104+
/** Gets the name of this environment variable. */
105+
override string getName() { result = envName }
106+
}
107+
57108
/**
58109
* An Actions job within a workflow.
59110
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobs.
@@ -88,6 +139,9 @@ module Actions {
88139
/** Gets the sequence of `steps` within this job. */
89140
YamlSequence getSteps() { result = this.lookup("steps") }
90141

142+
/** Gets the `env` mapping in this job. */
143+
YamlMapping getEnv() { result = this.lookup("env") }
144+
91145
/** Gets the workflow this job belongs to. */
92146
Workflow getWorkflow() { result = workflow }
93147

@@ -149,6 +203,9 @@ module Actions {
149203
/** Gets the value of the `if` field in this step, if any. */
150204
StepIf getIf() { result.getStep() = this }
151205

206+
/** Gets the value of the `env` field in this step, if any. */
207+
YamlMapping getEnv() { result = this.lookup("env") }
208+
152209
/** Gets the ID of this step, if any. */
153210
string getId() { result = this.lookup("id").(YamlString).getValue() }
154211
}
@@ -259,6 +316,10 @@ module Actions {
259316
.regexpCapture("\\$\\{\\{\\s*([A-Za-z0-9_\\[\\]\\*\\((\\)\\.\\-]+)\\s*\\}\\}", 1)
260317
}
261318

319+
/** Extracts the 'name' part from env.name */
320+
bindingset[name]
321+
string getEnvName(string name) { result = name.regexpCapture("env\\.([A-Za-z0-9_]+)", 1) }
322+
262323
/**
263324
* A `script:` field within an Actions `with:` specific to `actions/github-script` action.
264325
*

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

Lines changed: 6 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 (i.e. <b>NOT</b> the ${{ env.VAR }}).
2325
</p>
2426
<p>
2527
It is also recommended to limit the permissions of any tokens used

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,22 +103,38 @@ private predicate isExternalUserControlledWorkflowRun(string context) {
103103
)
104104
}
105105

106-
from YamlNode node, string context, Actions::On on
106+
from YamlNode node, string injection, string context, Actions::On on
107107
where
108108
(
109109
exists(Actions::Run run |
110110
node = run and
111-
Actions::getASimpleReferenceExpression(run) = context and
112-
run.getStep().getJob().getWorkflow().getOn() = on
111+
Actions::getASimpleReferenceExpression(run) = injection and
112+
run.getStep().getJob().getWorkflow().getOn() = on and
113+
(
114+
injection = context
115+
or
116+
exists(Actions::Env env |
117+
Actions::getEnvName(injection) = env.getName() and
118+
Actions::getASimpleReferenceExpression(env) = context
119+
)
120+
)
113121
)
114122
or
115123
exists(Actions::Script script, Actions::Step step, Actions::Uses uses |
116124
node = script and
125+
script.getWith().getStep().getJob().getWorkflow().getOn() = on and
117126
script.getWith().getStep() = step and
118127
uses.getStep() = step and
119128
uses.getGitHubRepository() = "actions/github-script" and
120-
Actions::getASimpleReferenceExpression(script) = context and
121-
script.getWith().getStep().getJob().getWorkflow().getOn() = on
129+
Actions::getASimpleReferenceExpression(script) = injection and
130+
(
131+
injection = context
132+
or
133+
exists(Actions::Env env |
134+
Actions::getEnvName(injection) = env.getName() and
135+
Actions::getASimpleReferenceExpression(env) = context
136+
)
137+
)
122138
)
123139
) and
124140
(
@@ -153,5 +169,5 @@ where
153169
isExternalUserControlledWorkflowRun(context)
154170
)
155171
select node,
156-
"Potential injection from the " + context +
172+
"Potential injection from the " + injection +
157173
" context, which may be controlled by an external user."
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
on: issues
22

3+
env:
4+
global_env: ${{ github.event.issue.title }}
5+
test: test
6+
37
jobs:
48
echo-chamber:
9+
env:
10+
job_env: ${{ github.event.issue.title }}
511
runs-on: ubuntu-latest
612
steps:
713
- run: echo '${{ github.event.issue.title }}'
814
- run: echo '${{ github.event.issue.body }}'
15+
- run: echo '${{ env.global_env }}'
16+
- run: echo '${{ env.test }}'
17+
- run: echo '${{ env.job_env }}'
18+
- run: echo '${{ env.step_env }}'
19+
env:
20+
step_env: ${{ github.event.issue.title }}

javascript/ql/test/query-tests/Security/CWE-094/ExpressionInjection/ExpressionInjection.expected

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
| .github/workflows/gollum.yml:8:12:8:53 | echo '$ ... tle }}' | Potential injection from the github.event.pages[11].title context, which may be controlled by an external user. |
1616
| .github/workflows/gollum.yml:9:12:9:56 | echo '$ ... ame }}' | Potential injection from the github.event.pages[0].page_name context, which may be controlled by an external user. |
1717
| .github/workflows/gollum.yml:10:12:10:59 | echo '$ ... ame }}' | Potential injection from the github.event.pages[2222].page_name context, which may be controlled by an external user. |
18-
| .github/workflows/issues.yml:7:12:7:49 | echo '$ ... tle }}' | Potential injection from the github.event.issue.title context, which may be controlled by an external user. |
19-
| .github/workflows/issues.yml:8:12:8:48 | echo '$ ... ody }}' | Potential injection from the github.event.issue.body context, which may be controlled by an external user. |
18+
| .github/workflows/issues.yml:13:12:13:49 | echo '$ ... tle }}' | Potential injection from the github.event.issue.title context, which may be controlled by an external user. |
19+
| .github/workflows/issues.yml:14:12:14:48 | echo '$ ... ody }}' | Potential injection from the github.event.issue.body context, which may be controlled by an external user. |
20+
| .github/workflows/issues.yml:15:12:15:39 | echo '$ ... env }}' | Potential injection from the env.global_env context, which may be controlled by an external user. |
21+
| .github/workflows/issues.yml:17:12:17:36 | echo '$ ... env }}' | Potential injection from the env.job_env context, which may be controlled by an external user. |
22+
| .github/workflows/issues.yml:18:12:18:37 | echo '$ ... env }}' | Potential injection from the env.step_env context, which may be controlled by an external user. |
2023
| .github/workflows/pull_request_review.yml:7:12:7:56 | echo '$ ... tle }}' | Potential injection from the github.event.pull_request.title context, which may be controlled by an external user. |
2124
| .github/workflows/pull_request_review.yml:8:12:8:55 | echo '$ ... ody }}' | Potential injection from the github.event.pull_request.body context, which may be controlled by an external user. |
2225
| .github/workflows/pull_request_review.yml:9:12:9:61 | echo '$ ... bel }}' | Potential injection from the github.event.pull_request.head.label context, which may be controlled by an external user. |

0 commit comments

Comments
 (0)