fix: 收紧NapCat提示词加载边界

This commit is contained in:
sunlei 2026-06-24 23:47:19 +08:00
parent e29ce3be86
commit d5e9bf5b01
2 changed files with 48 additions and 2 deletions

View File

@ -42,6 +42,7 @@ import {
} from "./tools/napcatAutomation.types.js";
import {
loadNapcatAutomationPrompt,
type NapcatAutomationPromptName,
napcatAutomationPromptNames,
} from "./tools/napcatAutomation.prompts.js";
import {
@ -99,6 +100,17 @@ export async function runSelfTest(): Promise<void> {
throw new Error(`NapCat prompt safety contract missing: ${promptName}`);
}
}
let traversalPromptRejected = false;
try {
loadNapcatAutomationPrompt("../../README" as NapcatAutomationPromptName);
} catch (error) {
traversalPromptRejected = String(error).includes(
"Unsupported NapCat automation prompt",
);
}
if (!traversalPromptRejected) {
throw new Error("NapCat prompt traversal self-check failed");
}
const reviewClassifier = {
localizedSecretLabelAllowed: isBenignCredentialReviewValue("密钥"),

View File

@ -17,14 +17,48 @@ const promptsRoot = path.resolve(
'prompts',
'napcat',
);
const promptsRootBoundary = promptsRoot.endsWith(path.sep)
? promptsRoot
: `${promptsRoot}${path.sep}`;
const napcatAutomationPromptNameSet = new Set<string>(
napcatAutomationPromptNames,
);
const napcatAutomationPromptFileNames = {
'upstream-audit': 'upstream-audit.md',
'sync-candidate-review': 'sync-candidate-review.md',
'runtime-release-readiness': 'runtime-release-readiness.md',
'remote-dev-handoff': 'remote-dev-handoff.md',
} satisfies Record<NapcatAutomationPromptName, string>;
/**
* Parses an external prompt name into the source-controlled NapCat prompt enum.
* @param value - Raw prompt name from CLI, JSON, MCP input, or a typed caller; only exact entries in `napcatAutomationPromptNames` are accepted.
* @returns A stable prompt name that can be mapped to a fixed file under `prompts/napcat`.
*/
export function parseNapcatAutomationPromptName(
value: string,
): NapcatAutomationPromptName {
if (napcatAutomationPromptNameSet.has(value)) {
return value as NapcatAutomationPromptName;
}
throw new Error(`Unsupported NapCat automation prompt: ${value}`);
}
/**
* Loads a source-controlled NapCat automation prompt by stable prompt name.
* @param name - Prompt identifier from `napcatAutomationPromptNames`; because it is an enum value rather than a user-supplied path, callers cannot escape `prompts/napcat`.
* @param name - Prompt identifier from `napcatAutomationPromptNames`; runtime callers may still pass untyped external strings, so the loader re-validates and resolves only fixed filenames.
* @returns UTF-8 prompt text used by Codex CLI automation.
*/
export function loadNapcatAutomationPrompt(
name: NapcatAutomationPromptName,
): string {
return readFileSync(path.join(promptsRoot, `${name}.md`), 'utf8');
const promptName = parseNapcatAutomationPromptName(String(name));
const promptPath = path.resolve(
promptsRoot,
napcatAutomationPromptFileNames[promptName],
);
if (!promptPath.startsWith(promptsRootBoundary)) {
throw new Error(`NapCat automation prompt escaped prompt root: ${name}`);
}
return readFileSync(promptPath, 'utf8');
}