Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2025-02-14 - Prevent Git Config Code Execution
**Vulnerability:** Invoking `git` via `subprocess` against untrusted directories without overriding config can allow malicious repositories to execute code via `.git/config` hooks like `core.fsmonitor`.
**Learning:** `git` uses configurations from the `.git/config` file in the current working directory or `cwd` argument, which could be controlled by an attacker when analyzing untrusted codebases.
**Prevention:** Explicitly pass `("-c", "core.fsmonitor=false")` as `_SAFE_GIT_CONFIG` to all `git` subprocess commands in the codebase.
10 changes: 6 additions & 4 deletions src/wardline/core/delta.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
if TYPE_CHECKING:
from wardline.scanner.index import Entity

_SAFE_GIT_CONFIG = ("-c", "core.fsmonitor=false")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the formatter-required blank line

The same CI job also runs uv run ruff format --check src tests; inserting this top-level constant with only one blank line before the following def makes ruff format --check report the changed source files would be reformatted, so fixing the E501s alone still leaves the PR red. Add the formatter-required extra blank line here and at the matching new constant in src/wardline/core/legis.py.

Useful? React with πŸ‘Β / πŸ‘Ž.



def get_changed_files_since(ref: str, root: Path) -> set[str]:
"""Get the set of file paths (repo-relative, POSIX-style matching Location.path)
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions src/wardline/core/legis.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ def project_finding(finding: Finding) -> dict[str, Any]:
}


_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.

Expand All @@ -199,7 +202,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,
Expand All @@ -215,7 +218,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,
Expand Down
20 changes: 18 additions & 2 deletions tests/unit/core/test_delta.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "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")
Expand Down
Loading