refactor(gitref): 將分支名稱驗證改為正式模組

This commit is contained in:
Jeffery
2026-07-21 16:26:30 +08:00
parent 76617dd9dc
commit 1f012c6cfb
3 changed files with 39 additions and 45 deletions
+32
View File
@@ -0,0 +1,32 @@
'use strict';
const { execFileSync } = require('child_process');
/**
* 驗證遠端分支名稱可安全用於 refspec 與 refs/remotes/origin/*。
*
* @param {string} refName - 使用者或事件 payload 提供的分支名稱。
* @param {string} fieldName - 錯誤訊息中的欄位名稱。
* @returns {string} 原樣回傳通過驗證的分支名稱。
* @throws {Error} 分支名稱空白、含路徑穿越,或不符合 git 分支 ref 規則時拋出。
*/
function assertSafeBranchRef(refName, fieldName) {
const value = String(refName || '').trim();
if (!value) throw new Error(`${fieldName} 不可為空。`);
if (value.includes('..') || value.startsWith('/') || value.endsWith('/') || value.includes('\\')) {
throw new Error(`${fieldName} 不是安全的分支名稱:${value}`);
}
try {
execFileSync('git', ['check-ref-format', '--branch', value], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
} catch {
throw new Error(`${fieldName} 不是合法的 git 分支名稱:${value}`);
}
return value;
}
module.exports = {
assertSafeBranchRef,
};
+4 -43
View File
@@ -1,37 +1,10 @@
'use strict';
const { execFileSync } = require('child_process');
const { assertSafeBranchRef } = require('./gitref');
// git 操作工具:一律以 execFileSync 呼叫 git(不經 shell,避免注入),輸出以 UTF-8 回傳。
/**
* 驗證遠端分支名稱可安全用於 refspec 與 refs/remotes/origin/*。
*
* @param {string} refName - 使用者或事件 payload 提供的分支名稱。
* @param {string} fieldName - 錯誤訊息中的欄位名稱。
* @returns {string} 原樣回傳通過驗證的分支名稱。
* @throws {Error} 分支名稱空白、含路徑穿越,或不符合 git 分支 ref 規則時拋出。
* @remarks
* 使用情境:`resolveMergeBase` 的 `baseRef` 與 `commitAndPushFindings` 的
* `headRef` 會被組進 refspec;先驗證可避免惡意 payload 影響本地 refs 路徑。
*/
function assertSafeBranchRef(refName, fieldName) {
const value = String(refName || '').trim();
if (!value) throw new Error(`${fieldName} 不可為空。`);
if (value.includes('..') || value.startsWith('/') || value.endsWith('/') || value.includes('\\')) {
throw new Error(`${fieldName} 不是安全的分支名稱:${value}`);
}
try {
execFileSync('git', ['check-ref-format', '--branch', value], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
} catch {
throw new Error(`${fieldName} 不是合法的 git 分支名稱:${value}`);
}
return value;
}
/**
* 同步執行 git 指令並回傳原始 stdout 輸出。
*
@@ -308,18 +281,9 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s
/**
* 以帶認證的方式推送到指定遠端,認證資訊只經環境變數傳入、不進命令列 argv。
*
* 認證方式:等同 `https://ai-review-bot:<secret>@host/...` 的 HTTP Basicgit 會把
* URL 帳密轉成相同的 `Authorization: Basic` 標頭送出),但改以 git 的
* `GIT_CONFIG_*` 環境變數注入 `http.<serverUrl>/.extraheader`,使 base64 憑證**不出現在 argv**
* (避免程序清單/例外回顯洩漏);推送目標 URL 亦不含帳密。
*
* 觸發 CI 關鍵:`actions/checkout` 會把「自動 Actions token」持久化在同一個
* `http.<serverUrl>/.extraheader` scope;若沿用它推送,Gitea 會視為「自動 token 觸發」而
* **不再觸發 workflow**(防遞迴)。故本函式對這次 push 於該 scope**先以空值重置**(清掉自動
* token——git 對 extraHeader 給空值即清空既有清單),**再注入 PAT 的 Authorization**,讓推送以
* PAT 身分進行、觸發 PR 的 synchronize;作用範圍僅限本次 push 的環境變數,不影響 action 其他
* 仰賴 checkout 持久化憑證的 fetch(如 {@link resolveMergeBase})。
* 推送失敗時**不重拋原始例外**(其 message 會含命令列與遠端 URL),改拋固定訊息。
* 以 `GIT_CONFIG_*` 注入本次 HTTP Basic extraheader,避免憑證出現在 argv
* 同時先清空 checkout 持久化的自動 token extraheader,確保本次 push 使用呼叫端 token。
* 推送失敗時改拋固定訊息,避免原始例外帶出遠端 URL 或認證資訊。
*
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
* @param {string} remoteUrl - 不含帳密的遠端 URL(形如 `https://host/owner/repo.git`)。
@@ -367,7 +331,4 @@ module.exports = {
fileDiff,
fileLastUpdatedIso,
commitAndPushFindings,
__test: {
assertSafeBranchRef,
},
};