fix(git): refuse repository config that runs a program before push, release and gc
An agent that can write a repository's .git/config could make the operator's pkgmgr push, release or branch gc run a program or reroute the push. UNSAFE_CONFIG lists the local keys that do (core.sshCommand, gitProxy, askPass, fsmonitor, alternateRefsCommand, credential helpers, gpg programs, protocol.*, config hooks, gc.recentObjectsHook, http proxy/TLS/cookie keys, remote pack programs and proxy, url rewrites). push and release refuse up front; gc skips its fetch and keeps remote branches. Filter drivers, hooksPath, gpg.format and include stay allowed for git-lfs, husky and per-repo signing. SCAN_FLAGS add --no-pager and switch off config-defined hooks (hook.<event>.enabled=false), which core.hooksPath does not cover. The signing amend, gc's fetch, delete push and branch -D run under them. Remote and ref names sit behind -- in every push, fetch, ls-remote and remote get-url, fetches and pushes no longer recurse into submodules, and a staged gitlink is no longer folded into the signed HEAD. gc skips a compose project that also has containers outside the worktree. 🎯 Fix: 92 % 💥 Break: 4 % 💸 Cost: 91 % ⏱️ Time: 89 % 🛡️ Security: 100 % 🏆 Score: 100 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fb6183838b
commit
4c8b137449
13 files changed
+467
-22
No files matched your search
@@ -0,0 +1,72 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from pkgmgr.core.git.commands import delete_remote_branch, push, push_upstream
|
||||
|
||||
REMOTE = "-r"
|
||||
|
||||
|
||||
def git(cwd: str, *args: str) -> str:
|
||||
return subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-c",
|
||||
"user.name=t",
|
||||
"-c",
|
||||
"user.email=t@example.org",
|
||||
"-c",
|
||||
"commit.gpgsign=false",
|
||||
*args,
|
||||
],
|
||||
cwd=cwd,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
|
||||
|
||||
class TestPushCommandsEndOptions(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self._tmp.cleanup)
|
||||
root = os.path.realpath(self._tmp.name)
|
||||
self.origin = os.path.join(root, "origin.git")
|
||||
self.repo = os.path.join(root, "repo")
|
||||
git(root, "init", "-q", "--bare", "-b", "main", self.origin)
|
||||
git(root, "init", "-q", "-b", "main", self.repo)
|
||||
git(self.repo, "config", f"remote.{REMOTE}.url", self.origin)
|
||||
git(
|
||||
self.repo,
|
||||
"config",
|
||||
f"remote.{REMOTE}.fetch",
|
||||
f"+refs/heads/*:refs/remotes/{REMOTE}/*",
|
||||
)
|
||||
git(self.repo, "commit", "-q", "--allow-empty", "-m", "init")
|
||||
|
||||
def _branches(self) -> str:
|
||||
return git(self.origin, "for-each-ref", "--format=%(refname:short)")
|
||||
|
||||
def test_remote_named_like_an_option(self) -> None:
|
||||
push_upstream(REMOTE, "main", cwd=self.repo)
|
||||
self.assertEqual(self._branches(), "main")
|
||||
self.assertEqual(
|
||||
git(self.repo, "rev-parse", "--abbrev-ref", "@{u}"), f"{REMOTE}/main"
|
||||
)
|
||||
|
||||
git(self.repo, "commit", "-q", "--amend", "--allow-empty", "-m", "rewritten")
|
||||
push(REMOTE, "main:feature", cwd=self.repo)
|
||||
push(REMOTE, "main", force=True, cwd=self.repo)
|
||||
self.assertEqual(
|
||||
git(self.origin, "rev-parse", "main"), git(self.repo, "rev-parse", "HEAD")
|
||||
)
|
||||
|
||||
delete_remote_branch(REMOTE, "feature", cwd=self.repo)
|
||||
self.assertEqual(self._branches(), "main")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user