From b64a31299496858a65b28f066e5025137b7b9cce Mon Sep 17 00:00:00 2001 From: matevip Date: Tue, 26 May 2026 23:10:44 +0800 Subject: [PATCH] fix(skill): harden GitHub token handling against credential leaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous private-repo support inlined the access token into the clone URL and then logged that URL on success — leaking the token to log files, container stdout, and any IOException thrown when the clone failed. The token also appeared in the process command line, visible to anyone with shell access via `ps`. Switch to git's GIT_CONFIG_COUNT/KEY/VALUE environment variables, which inject `http.extraHeader: Authorization: Bearer ` into the child process without ever touching argv or the repo URL. The URL stays pristine, so the existing INFO log and error message are safe. Other changes: - Resolve token from `mateclaw.skill.github-token` property first, then fall back to GITHUB_TOKEN env var. Keeps the original deployment contract while letting admins manage the credential via configuration. - Tighten the host check (prefix match on `https://github.com/` etc.) so a crafted URL like `https://evil.com/?u=github.com/...` cannot trick the fetcher into forwarding the token to a third party. - Set GIT_TERMINAL_PROMPT=0 so a bad token fails fast instead of blocking on an interactive password prompt. --- .../mate/skill/installer/GitSkillFetcher.java | 58 +++++++++++++++---- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/skill/installer/GitSkillFetcher.java b/mateclaw-server/src/main/java/vip/mate/skill/installer/GitSkillFetcher.java index 2161ef5a..907e5f78 100644 --- a/mateclaw-server/src/main/java/vip/mate/skill/installer/GitSkillFetcher.java +++ b/mateclaw-server/src/main/java/vip/mate/skill/installer/GitSkillFetcher.java @@ -1,6 +1,7 @@ package vip.mate.skill.installer; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import vip.mate.skill.installer.model.SkillBundle; import vip.mate.skill.runtime.SkillFrontmatterParser; @@ -27,12 +28,23 @@ public class GitSkillFetcher { private final SkillFrontmatterParser frontmatterParser; - /** 读取环境变量 GITHUB_TOKEN,用于访问私有仓库(公有仓库不受影响) */ - private final String githubToken = System.getenv("GITHUB_TOKEN") != null - ? System.getenv("GITHUB_TOKEN") : ""; + /** + * GitHub access token used when cloning private repositories. + * Resolution order: {@code mateclaw.skill.github-token} property → {@code GITHUB_TOKEN} + * environment variable → empty (public repos only). Kept as a plain field so the value + * is never logged or embedded in URLs — it is passed to the git subprocess through + * dedicated environment variables (see {@link #cloneRepo}). + */ + private final String githubToken; - public GitSkillFetcher(SkillFrontmatterParser frontmatterParser) { + public GitSkillFetcher( + SkillFrontmatterParser frontmatterParser, + @Value("${mateclaw.skill.github-token:}") String configuredGithubToken) { this.frontmatterParser = frontmatterParser; + String token = (configuredGithubToken != null && !configuredGithubToken.isBlank()) + ? configuredGithubToken + : System.getenv("GITHUB_TOKEN"); + this.githubToken = (token == null) ? "" : token.trim(); } /** @@ -102,15 +114,15 @@ public class GitSkillFetcher { } /** - * git clone --depth 1 到临时目录 + * git clone --depth 1 to a temporary directory. + *

+ * When a GitHub token is configured and the repository is hosted on github.com, + * the credential is forwarded to the git subprocess through {@code GIT_CONFIG_*} + * environment variables — equivalent to {@code git -c http.extraHeader=...} but + * without ever placing the token in the process command line (visible to {@code ps}) + * or the repository URL (visible in logs and error messages). Requires git 2.31+. */ private void cloneRepo(String repoUrl, String ref, Path targetDir) throws IOException, InterruptedException { - // 如果配置了 GITHUB_TOKEN 且是 GitHub 地址,注入 token 以支持私有仓库 - // 公有仓库带 token 访问同样正常,不受影响 - if (!githubToken.isBlank() && repoUrl.contains("github.com")) { - repoUrl = repoUrl.replaceFirst("https://", "https://" + githubToken + "@"); - } - var command = new java.util.ArrayList(); command.add("git"); command.add("clone"); @@ -125,6 +137,19 @@ public class GitSkillFetcher { ProcessBuilder pb = new ProcessBuilder(command); pb.redirectErrorStream(true); + + // Inject credentials for private GitHub repos via the git subprocess environment. + // Keeping the token out of argv and out of the URL ensures it cannot leak through + // process listings, the INFO log below, or the IOException message on clone failure. + if (!githubToken.isEmpty() && isGithubHost(repoUrl)) { + var env = pb.environment(); + env.put("GIT_CONFIG_COUNT", "1"); + env.put("GIT_CONFIG_KEY_0", "http.extraHeader"); + env.put("GIT_CONFIG_VALUE_0", "Authorization: Bearer " + githubToken); + // Fail fast on auth errors instead of blocking on an interactive password prompt. + env.put("GIT_TERMINAL_PROMPT", "0"); + } + Process process = pb.start(); boolean finished = process.waitFor(CLONE_TIMEOUT_SECONDS, TimeUnit.SECONDS); @@ -142,6 +167,17 @@ public class GitSkillFetcher { log.info("Cloned {} (ref={}) to {}", repoUrl, ref, targetDir); } + /** + * Match GitHub host conservatively (scheme + host boundary) so a malicious URL like + * {@code https://evil.com/?u=github.com/...} cannot smuggle the token to a third party. + */ + private static boolean isGithubHost(String repoUrl) { + return repoUrl != null + && (repoUrl.startsWith("https://github.com/") + || repoUrl.startsWith("http://github.com/") + || repoUrl.startsWith("git@github.com:")); + } + /** * 在 skill 根目录中定位 SKILL.md */