From b0c4cedfabee50a41b06232899dc4ec1c88d8cde Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Fri, 28 Aug 2026 06:24:08 +0700 Subject: [PATCH] #157: redact remote-URL user-info before it reaches a log 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. --- .../dev/ltms/fleet/session/GitWorktrees.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/fleetd/src/main/java/dev/ltms/fleet/session/GitWorktrees.java b/fleetd/src/main/java/dev/ltms/fleet/session/GitWorktrees.java index bcf7c6d..571dda0 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/session/GitWorktrees.java +++ b/fleetd/src/main/java/dev/ltms/fleet/session/GitWorktrees.java @@ -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("://[^@/]*@", "://@"); + } + 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); } /**