@@ -120,6 +120,10 @@ impl BashExecutor {
120120 return false ;
121121 }
122122
123+ if !self . is_safe_git_command ( & command_lower) {
124+ return false ;
125+ }
126+
123127 let forbidden_commands = [
124128 "rm" ,
125129 "mv" ,
@@ -183,6 +187,78 @@ impl BashExecutor {
183187
184188 true
185189 }
190+
191+ fn is_safe_git_command ( & self , command : & str ) -> bool {
192+ if !command. starts_with ( "git " ) && !command. contains ( " git " ) && !command. contains ( ";git " )
193+ && !command. contains ( "&&git " ) && !command. contains ( "||git " ) && !command. contains ( "|git " ) {
194+ return true ;
195+ }
196+
197+ // Allow safe git stash read-only operations
198+ if command. contains ( "git stash list" ) || command. contains ( "git stash show" ) {
199+ return true ;
200+ }
201+
202+ // Dangerous git operations that are completely blocked
203+ let dangerous_git_ops = [
204+ "git push" ,
205+ "git pull" ,
206+ "git fetch" ,
207+ "git clone" ,
208+ "git clean" ,
209+ "git reset --hard" ,
210+ "git reset --mixed" ,
211+ "git checkout -f" ,
212+ "git checkout -b" ,
213+ "git checkout --" ,
214+ "git branch -d" ,
215+ "git branch -D" ,
216+ "git branch -m" ,
217+ "git branch -M" ,
218+ "git remote add" ,
219+ "git remote set-url" ,
220+ "git remote remove" ,
221+ "git remote rm" ,
222+ "git submodule" ,
223+ "git filter-branch" ,
224+ "git gc" ,
225+ "git prune" ,
226+ "git update-ref" ,
227+ "git send-email" ,
228+ "git apply" ,
229+ "git am" ,
230+ "git cherry-pick" ,
231+ "git revert" ,
232+ "git commit" ,
233+ "git merge" ,
234+ "git rebase" ,
235+ "git tag -d" ,
236+ "git stash" , // Blocks all stash operations except list/show (checked above)
237+ "git init" ,
238+ "git add" ,
239+ "git rm" ,
240+ "git mv" ,
241+ "git restore" ,
242+ "git switch" ,
243+ ] ;
244+
245+ for dangerous_op in & dangerous_git_ops {
246+ if command. starts_with ( dangerous_op)
247+ || command. contains ( & format ! ( " {}" , dangerous_op) )
248+ || command. contains ( & format ! ( ";{}" , dangerous_op) )
249+ || command. contains ( & format ! ( "&&{}" , dangerous_op) )
250+ || command. contains ( & format ! ( "||{}" , dangerous_op) )
251+ || command. contains ( & format ! ( "|{}" , dangerous_op) )
252+ {
253+ return false ;
254+ }
255+ }
256+
257+ // Allow safe read-only git commands:
258+ // git status, git log, git diff, git show, git branch (list only),
259+ // git remote -v (view only), git config --list, git ls-files, etc.
260+ true
261+ }
186262}
187263
188264#[ cfg( test) ]
@@ -273,4 +349,107 @@ mod tests {
273349 panic ! ( "Expected ToolExecution error" ) ;
274350 }
275351 }
352+
353+ #[ test]
354+ fn test_safe_git_commands ( ) {
355+ let executor = BashExecutor :: new ( PathBuf :: from ( "." ) ) . unwrap ( ) ;
356+
357+ // Safe read-only git commands
358+ assert ! ( executor. is_safe_command( "git status" ) ) ;
359+ assert ! ( executor. is_safe_command( "git log" ) ) ;
360+ assert ! ( executor. is_safe_command( "git log --oneline" ) ) ;
361+ assert ! ( executor. is_safe_command( "git diff" ) ) ;
362+ assert ! ( executor. is_safe_command( "git diff HEAD~1" ) ) ;
363+ assert ! ( executor. is_safe_command( "git show" ) ) ;
364+ assert ! ( executor. is_safe_command( "git show HEAD" ) ) ;
365+ assert ! ( executor. is_safe_command( "git branch" ) ) ;
366+ assert ! ( executor. is_safe_command( "git branch -v" ) ) ;
367+ assert ! ( executor. is_safe_command( "git branch --list" ) ) ;
368+ assert ! ( executor. is_safe_command( "git remote -v" ) ) ;
369+ assert ! ( executor. is_safe_command( "git config --list" ) ) ;
370+ assert ! ( executor. is_safe_command( "git ls-files" ) ) ;
371+ assert ! ( executor. is_safe_command( "git ls-tree HEAD" ) ) ;
372+ assert ! ( executor. is_safe_command( "git blame file.txt" ) ) ;
373+ assert ! ( executor. is_safe_command( "git grep pattern" ) ) ;
374+ assert ! ( executor. is_safe_command( "git rev-parse HEAD" ) ) ;
375+ assert ! ( executor. is_safe_command( "git describe --tags" ) ) ;
376+ assert ! ( executor. is_safe_command( "git stash list" ) ) ;
377+ assert ! ( executor. is_safe_command( "git stash show" ) ) ;
378+ assert ! ( executor. is_safe_command( "git stash show stash@{0}" ) ) ;
379+ }
380+
381+ #[ test]
382+ fn test_dangerous_git_commands ( ) {
383+ let executor = BashExecutor :: new ( PathBuf :: from ( "." ) ) . unwrap ( ) ;
384+
385+ // Remote operations (data leakage risk)
386+ assert ! ( !executor. is_safe_command( "git push" ) ) ;
387+ assert ! ( !executor. is_safe_command( "git push origin main" ) ) ;
388+ assert ! ( !executor. is_safe_command( "git push --force" ) ) ;
389+ assert ! ( !executor. is_safe_command( "git pull" ) ) ;
390+ assert ! ( !executor. is_safe_command( "git pull origin main" ) ) ;
391+ assert ! ( !executor. is_safe_command( "git fetch" ) ) ;
392+ assert ! ( !executor. is_safe_command( "git fetch origin" ) ) ;
393+ assert ! ( !executor. is_safe_command( "git clone https://example.com/repo.git" ) ) ;
394+
395+ // Destructive local operations
396+ assert ! ( !executor. is_safe_command( "git clean -fd" ) ) ;
397+ assert ! ( !executor. is_safe_command( "git reset --hard" ) ) ;
398+ assert ! ( !executor. is_safe_command( "git reset --hard HEAD~1" ) ) ;
399+ assert ! ( !executor. is_safe_command( "git checkout -f" ) ) ;
400+ assert ! ( !executor. is_safe_command( "git checkout -b newbranch" ) ) ;
401+ assert ! ( !executor. is_safe_command( "git branch -D branch-name" ) ) ;
402+ assert ! ( !executor. is_safe_command( "git branch -d branch-name" ) ) ;
403+ assert ! ( !executor. is_safe_command( "git filter-branch" ) ) ;
404+
405+ // Modifications
406+ assert ! ( !executor. is_safe_command( "git add ." ) ) ;
407+ assert ! ( !executor. is_safe_command( "git add file.txt" ) ) ;
408+ assert ! ( !executor. is_safe_command( "git commit -m 'message'" ) ) ;
409+ assert ! ( !executor. is_safe_command( "git commit --amend" ) ) ;
410+ assert ! ( !executor. is_safe_command( "git rm file.txt" ) ) ;
411+ assert ! ( !executor. is_safe_command( "git mv old.txt new.txt" ) ) ;
412+ assert ! ( !executor. is_safe_command( "git merge branch" ) ) ;
413+ assert ! ( !executor. is_safe_command( "git rebase main" ) ) ;
414+ assert ! ( !executor. is_safe_command( "git cherry-pick abc123" ) ) ;
415+ assert ! ( !executor. is_safe_command( "git revert abc123" ) ) ;
416+ assert ! ( !executor. is_safe_command( "git restore file.txt" ) ) ;
417+ assert ! ( !executor. is_safe_command( "git switch main" ) ) ;
418+
419+ // Remote configuration changes
420+ assert ! ( !executor. is_safe_command( "git remote add origin https://evil.com/repo.git" ) ) ;
421+ assert ! ( !executor. is_safe_command( "git remote set-url origin https://evil.com/repo.git" ) ) ;
422+ assert ! ( !executor. is_safe_command( "git remote remove origin" ) ) ;
423+
424+ // Submodules (can fetch from remote)
425+ assert ! ( !executor. is_safe_command( "git submodule update" ) ) ;
426+ assert ! ( !executor. is_safe_command( "git submodule init" ) ) ;
427+
428+ // Stash operations (modify state)
429+ assert ! ( !executor. is_safe_command( "git stash" ) ) ;
430+ assert ! ( !executor. is_safe_command( "git stash pop" ) ) ;
431+ assert ! ( !executor. is_safe_command( "git stash apply" ) ) ;
432+ assert ! ( !executor. is_safe_command( "git stash drop" ) ) ;
433+ assert ! ( !executor. is_safe_command( "git stash clear" ) ) ;
434+
435+ // Init (creates repository)
436+ assert ! ( !executor. is_safe_command( "git init" ) ) ;
437+ assert ! ( !executor. is_safe_command( "git init new-repo" ) ) ;
438+ }
439+
440+ #[ test]
441+ fn test_git_commands_in_chains ( ) {
442+ let executor = BashExecutor :: new ( PathBuf :: from ( "." ) ) . unwrap ( ) ;
443+
444+ // Safe commands in chains
445+ assert ! ( executor. is_safe_command( "git status && git log" ) ) ;
446+ assert ! ( executor. is_safe_command( "git diff | grep pattern" ) ) ;
447+ assert ! ( executor. is_safe_command( "echo test; git status" ) ) ;
448+
449+ // Dangerous commands in chains
450+ assert ! ( !executor. is_safe_command( "git status && git push" ) ) ;
451+ assert ! ( !executor. is_safe_command( "git log | git commit -m 'test'" ) ) ;
452+ assert ! ( !executor. is_safe_command( "echo test; git add ." ) ) ;
453+ assert ! ( !executor. is_safe_command( "git status || git pull" ) ) ;
454+ }
276455}
0 commit comments