@@ -2,28 +2,66 @@ use super::{audit_meta, WorkflowAudit};
2
2
use crate :: finding:: { Confidence , Finding , Severity } ;
3
3
use crate :: models:: Step ;
4
4
use crate :: state:: AuditState ;
5
+ use anyhow:: Context ;
5
6
use github_actions_models:: workflow:: job:: StepBody ;
6
- use regex:: RegexSet ;
7
7
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 ;
19
9
20
10
pub ( crate ) struct GitHubEnv ;
21
11
22
12
audit_meta ! ( GitHubEnv , "github-env" , "dangerous use of GITHUB_ENV" ) ;
23
13
24
14
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
+ }
27
65
}
28
66
}
29
67
@@ -47,8 +85,9 @@ impl WorkflowAudit for GitHubEnv {
47
85
return Ok ( findings) ;
48
86
}
49
87
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) ? {
52
91
findings. push (
53
92
Self :: finding ( )
54
93
. severity ( Severity :: High )
@@ -72,7 +111,7 @@ mod tests {
72
111
use crate :: audit:: github_env:: GitHubEnv ;
73
112
74
113
#[ test]
75
- fn test_shell_patterns ( ) {
114
+ fn test_exploitable_bash_patterns ( ) {
76
115
for case in & [
77
116
// Common cases
78
117
"echo foo >> $GITHUB_ENV" ,
@@ -98,7 +137,22 @@ mod tests {
98
137
"something |tee $GITHUB_ENV" ,
99
138
"something| tee $GITHUB_ENV" ,
100
139
] {
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) ;
102
156
}
103
157
}
104
158
}
0 commit comments