#157: redact remote-URL user-info before it reaches a log
CI / contract (push) Successful in 41s
CI / build (push) Successful in 1m25s

The four log lines added with the worktree HTTPS rewrite echoed the origin
URL verbatim, and one of them echoed the ssh:// authority, which carries
user-info. An ssh authority is normally just git@, so in practice this
changes nothing -- but a remote URL is not obviously a credential channel,
and that is precisely why one has leaked here three times (#157, #182).

Redact at the log call, not after it surprises someone.
This commit is contained in:
Dai Ha
2026-08-28 06:24:08 +07:00
parent 85417d5215
commit b0c4cedfab
@@ -211,6 +211,17 @@ public final class GitWorktrees implements Worktrees {
* guessing at it risks rewriting to the wrong place. A repo provisioned from that form keeps
* today's (broken, if the policy blocks the agent) SSH-only behaviour rather than a wrong rewrite.
*/
/**
* Blank the user-info of a remote URL before it reaches a log. A remote URL is not obviously a
* credential channel, which is exactly why one has leaked here three times ({@code git remote -v}
* printing a token inline, and fleetd #157 / #182). An {@code ssh://} authority normally carries
* only {@code git@}, so this usually changes nothing — it is here so that the one origin that
* does carry a secret cannot print it. Matches every {@code ://…@} pair, not just the first.
*/
private static String redactUserInfo(String url) {
return url == null ? null : url.replaceAll("://[^@/]*@", "://<redacted>@");
}
private void configureHttpsUrlRewriteForSshOrigin(String repoRoot, String worktreePath) {
if (exitCode("git", "-C", repoRoot, "config", "--get", "remote.origin.url") != 0) {
return;
@@ -220,20 +231,20 @@ public final class GitWorktrees implements Worktrees {
try {
uri = new URI(origin);
} catch (URISyntaxException e) {
log.warn("origin URL {} is not a valid URI; skipping worktree HTTPS rewrite", origin);
log.warn("origin URL {} is not a valid URI; skipping worktree HTTPS rewrite", redactUserInfo(origin));
return;
}
String scheme = uri.getScheme();
if (!"ssh".equalsIgnoreCase(scheme)) {
// Already https:// (the credential helper covers it), or a scheme-less/scp-like origin
// left alone on purpose — see the javadoc above.
log.debug("origin scheme is not ssh ({}) — no worktree HTTPS rewrite needed", origin);
log.debug("origin scheme is not ssh ({}) — no worktree HTTPS rewrite needed", redactUserInfo(origin));
return;
}
String host = uri.getHost();
String authority = uri.getRawAuthority();
if (host == null || host.isBlank() || authority == null || authority.isBlank()) {
log.warn("ssh origin {} has no resolvable host; skipping worktree HTTPS rewrite", origin);
log.warn("ssh origin {} has no resolvable host; skipping worktree HTTPS rewrite", redactUserInfo(origin));
return;
}
String sshBase = "ssh://" + authority + "/";
@@ -241,7 +252,7 @@ public final class GitWorktrees implements Worktrees {
exec("git", "-C", worktreePath, "config", "--worktree", "--replace-all",
"url." + httpsBase + ".insteadOf", sshBase);
log.info("worktree {} rewrites {} to {} (worktree-scoped; parent checkout untouched)",
worktreePath, sshBase, httpsBase);
worktreePath, redactUserInfo(sshBase), httpsBase);
}
/**