From d7a765cffe331a5ebd5cad1d6fcfb6e14bb71645 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 16:28:00 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fix=20Local=20Code=20Execution=20via=20Git=20Configuratio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disables `core.fsmonitor` for all `subprocess.run(["git", ...])` calls when operating on potentially untrusted directories, preventing malicious `.git/config` from executing arbitrary code. Also adds a journal entry to `.jules/sentinel.md` documenting this learning. Co-authored-by: tachyon-beep <544926+tachyon-beep@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ src/wardline/core/delta.py | 10 ++++++---- src/wardline/core/legis.py | 5 +++-- tests/unit/core/test_delta.py | 4 ++-- 4 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..1b2bc7af --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2024-06-27 - Local Code Execution via Git Configuration +**Vulnerability:** Untrusted repositories with malicious `.git/config` can execute arbitrary code when `subprocess.run(["git", ...])` is executed within their directory tree (specifically via settings like `core.fsmonitor`). +**Learning:** Git commands executed by `subprocess` without explicit isolation will respect local `.git/config` files, allowing an attacker to run arbitrary code on the scanning machine when an untrusted repository is parsed. +**Prevention:** Always explicitly disable dangerous config variables when executing `git` commands in Python by injecting `("-c", "core.fsmonitor=false")` (defined as `_SAFE_GIT_CONFIG`) into the argument list of `subprocess.run()`. diff --git a/src/wardline/core/delta.py b/src/wardline/core/delta.py index c4d63f0e..764c3c47 100644 --- a/src/wardline/core/delta.py +++ b/src/wardline/core/delta.py @@ -11,6 +11,8 @@ if TYPE_CHECKING: from wardline.scanner.index import Entity +_SAFE_GIT_CONFIG = ("-c", "core.fsmonitor=false") + def get_changed_files_since(ref: str, root: Path) -> set[str]: """Get the set of file paths (repo-relative, POSIX-style matching Location.path) @@ -22,7 +24,7 @@ def get_changed_files_since(ref: str, root: Path) -> set[str]: # 1. Get the git toplevel directory. try: res = subprocess.run( - ["git", "rev-parse", "--show-toplevel"], + ["git", *_SAFE_GIT_CONFIG, "rev-parse", "--show-toplevel"], cwd=root, capture_output=True, text=True, @@ -38,7 +40,7 @@ def get_changed_files_since(ref: str, root: Path) -> set[str]: # 2. Resolve ref to a verified object id before passing it to git diff. try: res = subprocess.run( - ["git", "rev-parse", "--verify", "--end-of-options", ref], + ["git", *_SAFE_GIT_CONFIG, "rev-parse", "--verify", "--end-of-options", ref], cwd=git_toplevel, capture_output=True, text=True, @@ -54,7 +56,7 @@ def get_changed_files_since(ref: str, root: Path) -> set[str]: # 3. Get changed files since ref (committed since ref, staged, unstaged). try: res = subprocess.run( - ["git", "diff", "--name-only", verified_ref, "--"], + ["git", *_SAFE_GIT_CONFIG, "diff", "--name-only", verified_ref, "--"], cwd=git_toplevel, capture_output=True, text=True, @@ -68,7 +70,7 @@ def get_changed_files_since(ref: str, root: Path) -> set[str]: # 4. Get untracked files. try: res = subprocess.run( - ["git", "ls-files", "--others", "--exclude-standard"], + ["git", *_SAFE_GIT_CONFIG, "ls-files", "--others", "--exclude-standard"], cwd=git_toplevel, capture_output=True, text=True, diff --git a/src/wardline/core/legis.py b/src/wardline/core/legis.py index 87144974..44361a67 100644 --- a/src/wardline/core/legis.py +++ b/src/wardline/core/legis.py @@ -189,6 +189,7 @@ def project_finding(finding: Finding) -> dict[str, Any]: "suppression_state": suppressed, } +_SAFE_GIT_CONFIG = ("-c", "core.fsmonitor=false") def _git_tree_sha(root: Path) -> str | None: """The committed tree object SHA (``git rev-parse HEAD^{tree}``), or None. @@ -199,7 +200,7 @@ def _git_tree_sha(root: Path) -> str | None: """ try: rev = subprocess.run( - ["git", "rev-parse", "HEAD^{tree}"], + ["git", *_SAFE_GIT_CONFIG, "rev-parse", "HEAD^{tree}"], cwd=root, capture_output=True, text=True, @@ -215,7 +216,7 @@ def _git_repo_root(root: Path) -> Path | None: """The containing git repository root, or None when unavailable.""" try: rev = subprocess.run( - ["git", "rev-parse", "--show-toplevel"], + ["git", *_SAFE_GIT_CONFIG, "rev-parse", "--show-toplevel"], cwd=root, capture_output=True, text=True, diff --git a/tests/unit/core/test_delta.py b/tests/unit/core/test_delta.py index 5b16c7af..b338f453 100644 --- a/tests/unit/core/test_delta.py +++ b/tests/unit/core/test_delta.py @@ -43,8 +43,8 @@ def run_dispatch(args, **kwargs): res = get_changed_files_since("HEAD~1", root) assert res == {"foo.py", "bar.py", "baz.py"} - assert mock_run.call_args_list[1].args[0] == ["git", "rev-parse", "--verify", "--end-of-options", "HEAD~1"] - assert mock_run.call_args_list[2].args[0] == ["git", "diff", "--name-only", "abc123", "--"] + assert mock_run.call_args_list[1].args[0] == ["git", "-c", "core.fsmonitor=false", "rev-parse", "--verify", "--end-of-options", "HEAD~1"] + assert mock_run.call_args_list[2].args[0] == ["git", "-c", "core.fsmonitor=false", "diff", "--name-only", "abc123", "--"] @patch("subprocess.run") From 191e83dcc0f374b0f8183c04f43a5c4a8d8837a4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 16:32:28 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fix=20Local=20Code=20Execution=20via=20Git=20Configuratio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disables `core.fsmonitor` for all `subprocess.run(["git", ...])` calls when operating on potentially untrusted directories, preventing malicious `.git/config` from executing arbitrary code. Also adds a journal entry to `.jules/sentinel.md` documenting this learning. Co-authored-by: tachyon-beep <544926+tachyon-beep@users.noreply.github.com> --- src/wardline/core/legis.py | 2 ++ tests/unit/core/test_delta.py | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/wardline/core/legis.py b/src/wardline/core/legis.py index 44361a67..fe23fcb9 100644 --- a/src/wardline/core/legis.py +++ b/src/wardline/core/legis.py @@ -189,8 +189,10 @@ def project_finding(finding: Finding) -> dict[str, Any]: "suppression_state": suppressed, } + _SAFE_GIT_CONFIG = ("-c", "core.fsmonitor=false") + def _git_tree_sha(root: Path) -> str | None: """The committed tree object SHA (``git rev-parse HEAD^{tree}``), or None. diff --git a/tests/unit/core/test_delta.py b/tests/unit/core/test_delta.py index b338f453..9d0ca7ba 100644 --- a/tests/unit/core/test_delta.py +++ b/tests/unit/core/test_delta.py @@ -43,8 +43,24 @@ def run_dispatch(args, **kwargs): res = get_changed_files_since("HEAD~1", root) assert res == {"foo.py", "bar.py", "baz.py"} - assert mock_run.call_args_list[1].args[0] == ["git", "-c", "core.fsmonitor=false", "rev-parse", "--verify", "--end-of-options", "HEAD~1"] - assert mock_run.call_args_list[2].args[0] == ["git", "-c", "core.fsmonitor=false", "diff", "--name-only", "abc123", "--"] + assert mock_run.call_args_list[1].args[0] == [ + "git", + "-c", + "core.fsmonitor=false", + "rev-parse", + "--verify", + "--end-of-options", + "HEAD~1", + ] + assert mock_run.call_args_list[2].args[0] == [ + "git", + "-c", + "core.fsmonitor=false", + "diff", + "--name-only", + "abc123", + "--", + ] @patch("subprocess.run")