Skip to content

Commit 79fd0d6

Browse files
feat: even more precision for bash steps in github-env
1 parent 7e4d4fc commit 79fd0d6

File tree

3 files changed

+86
-18
lines changed

3 files changed

+86
-18
lines changed

Diff for: Cargo.lock

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ serde-sarif = "0.6.5"
3232
serde_json = "1.0.133"
3333
serde_yaml = "0.9.34"
3434
terminal-link = "0.1.0"
35+
tree-sitter = "0.23.2"
36+
tree-sitter-bash = "0.23.3"
3537
yamlpath = "0.12.0"
3638

3739
[profile.release]

Diff for: src/audit/github_env.rs

+72-18
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,66 @@ use super::{audit_meta, WorkflowAudit};
22
use crate::finding::{Confidence, Finding, Severity};
33
use crate::models::Step;
44
use crate::state::AuditState;
5+
use anyhow::Context;
56
use github_actions_models::workflow::job::StepBody;
6-
use regex::RegexSet;
77
use std::ops::Deref;
8-
use std::sync::LazyLock;
9-
10-
static GITHUB_ENV_WRITE_SHELL: LazyLock<RegexSet> = LazyLock::new(|| {
11-
RegexSet::new([
12-
// matches the `... >> $GITHUB_ENV` pattern
13-
r#"(?m)^.+\s*>>?\s*"?\$\{?GITHUB_ENV\}?"?.*$"#,
14-
// matches the `... | tee $GITHUB_ENV` pattern
15-
r#"(?m)^.*\|\s*tee\s+"?\$\{?GITHUB_ENV\}?"?.*$"#,
16-
])
17-
.unwrap()
18-
});
8+
use tree_sitter::Parser;
199

2010
pub(crate) struct GitHubEnv;
2111

2212
audit_meta!(GitHubEnv, "github-env", "dangerous use of GITHUB_ENV");
2313

2414
impl GitHubEnv {
25-
fn uses_github_environment(run_step_body: &str) -> bool {
26-
GITHUB_ENV_WRITE_SHELL.is_match(run_step_body)
15+
fn evaluate_github_environment_within_bash_script(script_body: &str) -> anyhow::Result<bool> {
16+
let bash = tree_sitter_bash::LANGUAGE;
17+
let mut parser = Parser::new();
18+
parser
19+
.set_language(&bash.into())
20+
.context("failed to load bash parser")?;
21+
let tree = parser
22+
.parse(script_body, None)
23+
.context("failed to parse bash script body")?;
24+
25+
let mut stack = vec![tree.root_node()];
26+
27+
while let Some(node) = stack.pop() {
28+
if node.is_named() && (node.kind() == "file_redirect" || node.kind() == "pipeline") {
29+
let tree_expansion = &script_body[node.start_byte()..node.end_byte()];
30+
let targets_github_env = tree_expansion.contains("GITHUB_ENV");
31+
let exploitable_redirects =
32+
tree_expansion.contains(">>") || tree_expansion.contains(">");
33+
34+
// Eventually we can detect specific commands within the expansion,
35+
// tee and others
36+
let piped = tree_expansion.contains("|");
37+
38+
if (piped || exploitable_redirects) && targets_github_env {
39+
return Ok(true);
40+
}
41+
}
42+
43+
for child in node.named_children(&mut node.walk()) {
44+
stack.push(child);
45+
}
46+
}
47+
48+
Ok(false)
49+
}
50+
51+
fn uses_github_environment(run_step_body: &str, shell: &str) -> anyhow::Result<bool> {
52+
// Note : as an upcoming refinement, we should evaluate shell interpreters
53+
// other than Bash
54+
55+
match shell {
56+
"bash" => Self::evaluate_github_environment_within_bash_script(run_step_body),
57+
&_ => {
58+
log::warn!(
59+
"'{}' shell not supported when evaluating usage of GITHUB_ENV",
60+
shell
61+
);
62+
Ok(false)
63+
}
64+
}
2765
}
2866
}
2967

@@ -47,8 +85,9 @@ impl WorkflowAudit for GitHubEnv {
4785
return Ok(findings);
4886
}
4987

50-
if let StepBody::Run { run, .. } = &step.deref().body {
51-
if Self::uses_github_environment(run) {
88+
if let StepBody::Run { run, shell, .. } = &step.deref().body {
89+
let interpreter = shell.clone().unwrap_or("bash".into());
90+
if Self::uses_github_environment(run, &interpreter)? {
5291
findings.push(
5392
Self::finding()
5493
.severity(Severity::High)
@@ -72,7 +111,7 @@ mod tests {
72111
use crate::audit::github_env::GitHubEnv;
73112

74113
#[test]
75-
fn test_shell_patterns() {
114+
fn test_exploitable_bash_patterns() {
76115
for case in &[
77116
// Common cases
78117
"echo foo >> $GITHUB_ENV",
@@ -98,7 +137,22 @@ mod tests {
98137
"something |tee $GITHUB_ENV",
99138
"something| tee $GITHUB_ENV",
100139
] {
101-
assert!(GitHubEnv::uses_github_environment(case));
140+
let uses_github_env = GitHubEnv::uses_github_environment(case, "bash")
141+
.expect("test case is not valid Bash");
142+
assert!(uses_github_env);
143+
}
144+
}
145+
146+
#[test]
147+
fn test_additional_bash_patterns() {
148+
for case in &[
149+
// Comments
150+
"echo foo >> $OTHER_ENV # not $GITHUB_ENV",
151+
"something | tee \"${$OTHER_ENV}\" # not $GITHUB_ENV",
152+
] {
153+
let uses_github_env = GitHubEnv::uses_github_environment(case, "bash")
154+
.expect("test case is not valid Bash");
155+
assert!(!uses_github_env);
102156
}
103157
}
104158
}

0 commit comments

Comments
 (0)