fix: 强化NapCat审计只读与路径校验

This commit is contained in:
sunlei 2026-06-25 00:22:49 +08:00
parent 1f60506c89
commit 7619dd5928
2 changed files with 113 additions and 6 deletions

View File

@ -129,8 +129,8 @@ export async function runSelfTest(): Promise<void> {
const dryRunAuditCommandText = dryRunAuditCommands.join("\n");
for (const expected of [
"git status --short --branch",
"git fetch upstream --tags --prune",
"git fetch origin --prune",
"git fetch --dry-run upstream --tags --prune",
"git fetch --dry-run origin --prune",
"git diff --name-only",
"git merge-tree",
]) {
@ -140,6 +140,17 @@ export async function runSelfTest(): Promise<void> {
);
}
}
for (const command of dryRunAudit.commands) {
if (
command.readOnly &&
/^git\s+fetch(?:\s|$)/.test(command.command) &&
!/\s--dry-run(?:\s|$)/.test(command.command)
) {
throw new Error(
"NapCat upstream audit read-only fetch must use --dry-run",
);
}
}
for (const forbidden of [
{ label: "git merge", regex: /(^|\n)\s*git\s+merge(?:\s|$)/ },
{ label: "git push", regex: /(^|\n)\s*git\s+push(?:\s|$)/ },
@ -153,6 +164,50 @@ export async function runSelfTest(): Promise<void> {
);
}
}
for (const invalidPathCase of [
{
forkPatchFiles: [],
name: "Windows absolute path",
upstreamChangedFiles: ["D:/repo/packages/napcat-core/login/runtime.ts"],
},
{
forkPatchFiles: [],
name: "Unix absolute path",
upstreamChangedFiles: ["/repo/packages/napcat-core/login/runtime.ts"],
},
{
forkPatchFiles: [],
name: "UNC path",
upstreamChangedFiles: [
"\\\\server\\share\\packages\\napcat-core\\login\\runtime.ts",
],
},
{
forkPatchFiles: [],
name: "parent segment path",
upstreamChangedFiles: ["packages/../napcat-core/login/runtime.ts"],
},
{
forkPatchFiles: ["packages/../napcat-core/login/runtime.ts"],
name: "fork patch parent segment path",
upstreamChangedFiles: [],
},
]) {
const invalidPathAudit = classifyNapcatUpstreamAudit({
dryMergeConflict: false,
forkPatchFiles: invalidPathCase.forkPatchFiles,
upstreamChangedFiles: invalidPathCase.upstreamChangedFiles,
});
if (
invalidPathAudit.classification !== "blocked" ||
invalidPathAudit.recommendedAction !== "block" ||
!invalidPathAudit.reasonCodes.includes("METADATA_UNAVAILABLE")
) {
throw new Error(
`NapCat invalid audit path self-check failed: ${invalidPathCase.name}`,
);
}
}
for (const promptName of napcatAutomationPromptNames) {
const promptText = loadNapcatAutomationPrompt(promptName);
if (!promptText.includes("Do not edit files")) {

View File

@ -75,6 +75,46 @@ const hotZoneMatchers: Array<{ pattern: string; regex: RegExp }> = [
{ pattern: 'vite*.ts', regex: /(?:^|\/)vite[^/]*\.ts$/i },
];
/**
* Checks whether Git audit metadata contains a repo-relative path that the classifier can trust.
* @param value - Raw path string from fork/upstream Git diff collectors; empty entries are allowed because collectors may emit blank lines.
* @returns True when a non-empty path is absolute, UNC-based, or contains a parent-directory segment.
*/
function hasInvalidAuditPath(value: string): boolean {
const trimmed = value.trim();
if (!trimmed) return false;
if (/^[a-zA-Z]:[\\/]/.test(trimmed)) return true;
if (trimmed.startsWith('\\\\') || trimmed.startsWith('//')) return true;
if (trimmed.startsWith('/')) return true;
const normalized = trimmed
.replace(/\\/g, '/')
.replace(/^\.\/+/, '')
.replace(/\/+/g, '/');
return normalized.split('/').includes('..');
}
/**
* Checks both file lists for invalid non repo-relative metadata.
* @param input - Collector result carrying fork patch and upstream changed file paths; both arrays must contain repo-relative Git paths when non-empty.
* @returns True when any path entry would make deterministic hot-zone and overlap classification untrustworthy.
*/
function hasInvalidAuditMetadata(
input: NapcatAuditClassificationInput,
): boolean {
for (const file of input.forkPatchFiles) {
if (hasInvalidAuditPath(file)) {
return true;
}
}
for (const file of input.upstreamChangedFiles) {
if (hasInvalidAuditPath(file)) {
return true;
}
}
return false;
}
/**
* Normalizes Git path output into a stable comparison key for audit classifiers.
* @param value - File path from Git diff/status collectors; may contain Windows separators, leading `./`, or surrounding whitespace, but must represent a repo-relative path.
@ -195,6 +235,16 @@ function buildClassificationSummary(
export function classifyNapcatUpstreamAudit(
input: NapcatAuditClassificationInput,
): NapcatAuditClassificationResult {
if (hasInvalidAuditMetadata(input)) {
return {
classification: 'blocked',
reasonCodes: ['METADATA_UNAVAILABLE'],
recommendedAction: 'block',
summary:
'Invalid audit metadata: fork and upstream file paths must be repo-relative Git paths without absolute roots, UNC prefixes, or parent-directory segments.',
};
}
if (input.dryMergeConflict) {
return {
classification: 'blocked',
@ -254,13 +304,15 @@ export function buildNapcatUpstreamAudit(
readOnly: true,
},
{
command: 'git fetch upstream --tags --prune',
description: 'Refresh upstream release refs before file-level audit collection.',
command: 'git fetch --dry-run upstream --tags --prune',
description:
'Probe upstream release ref availability without updating local refs.',
readOnly: true,
},
{
command: 'git fetch origin --prune',
description: 'Refresh fork refs before comparing KT patch and upstream ranges.',
command: 'git fetch --dry-run origin --prune',
description:
'Probe fork ref availability without updating remote-tracking refs.',
readOnly: true,
},
{