fix(client): make our credential helper the only one the repo consults
Live QA against the beta forge failed its first push with 'Failed to authenticate user' while the config held a valid token. Cause: credential.helper is a list accumulated across system/global/repo config, and this machine has osxkeychain (Xcode gitconfig) plus store (~/.gitconfig). A stale entry for the forge host answered before our helper. The same list is a token leak in the other direction: git calls approve on every helper after a successful auth, so 'store' writes the forge token into ~/.git-credentials in plaintext -- undoing the 0600 config and the no-token-in-URL rule. Confirmed accidentally during QA when a verification clone with a URL-embedded token re-created exactly that entry. Fix: set an empty credential.helper first (git reads that as 'forget the inherited list'), then add ours -- in install_credential_helper and in the git clone inside get. 2 regression tests, one of which drives 'git credential fill' against a poisoned outer helper. 143 tests.
This commit is contained in:
@@ -858,5 +858,47 @@ class TestMarkerRoundTrip(GitScenarioBase):
|
||||
self.assertEqual(client.read_marker(self.local), {})
|
||||
|
||||
|
||||
class TestCredentialHelperIsolation(GitScenarioBase):
|
||||
"""A repo-local helper is not enough on a normal machine: git consults
|
||||
system + global helpers too, and they both shadow us and copy the token
|
||||
into plaintext. Found by live QA against the beta forge, not by a unit
|
||||
test -- so it gets one now."""
|
||||
|
||||
def test_install_leaves_exactly_one_helper(self):
|
||||
run_git(self.local, "config", "--add", "credential.helper", "store")
|
||||
client.install_credential_helper(self.local)
|
||||
# --get-all merges system + global + local, so entries inherited from
|
||||
# the machine still appear. What matters is that the last two are the
|
||||
# reset and ours: git reads an empty value as "forget every helper
|
||||
# inherited so far", so nothing before it can answer.
|
||||
helpers = run_git(self.local, "config", "--get-all",
|
||||
"credential.helper").splitlines()
|
||||
self.assertEqual(helpers[-2], "", helpers)
|
||||
self.assertIn("git-credential", helpers[-1])
|
||||
# the repo-level 'store' this test added is gone, not merely outvoted
|
||||
self.assertNotIn("store", helpers)
|
||||
|
||||
def test_inherited_helper_cannot_answer_for_the_forge(self):
|
||||
"""The end-to-end property: with a poisoned outer helper configured,
|
||||
the credential git actually resolves is ours."""
|
||||
fake = os.path.join(self.tmp, "poison.sh")
|
||||
with open(fake, "w") as f:
|
||||
f.write("#!/bin/sh\n"
|
||||
"echo username=wrong-user\necho password=stale-token\n")
|
||||
os.chmod(fake, 0o755)
|
||||
run_git(self.local, "config", "--add", "credential.helper",
|
||||
f"!{shlex.quote(fake)}")
|
||||
client.save_config({"gitea_base": "http://forge.example:3041",
|
||||
"login": "alice", "token": "the-right-token"})
|
||||
client.install_credential_helper(self.local)
|
||||
out = subprocess.run(
|
||||
["git", "-C", self.local, "credential", "fill"],
|
||||
input="protocol=http\nhost=forge.example:3041\n\n",
|
||||
capture_output=True, text=True, env=dict(
|
||||
GIT_ENV, GRANTHI_SYNC_HOME=os.environ["GRANTHI_SYNC_HOME"]))
|
||||
self.assertIn("password=the-right-token", out.stdout)
|
||||
self.assertNotIn("stale-token", out.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user