fix(verify-pr): refuse nested repositories in the credential scan

Round-4 verification at 19b14b1a8f left three Suggestions.

- A nested git repository is now refused instead of silently skipped: git
  does not descend into it, so a .env under tmp/pr-clone/ would pass. A
  vendored .git under node_modules/dist stays excluded like the files there.
- isExcluded is removed: the pathspecs already exclude those directories,
  and the surviving JS filter was unpinnable.
- The spaced Connection-nomination list is pinned; dropping trim made the
  proxy answer 502.
- The node_modules/dist credential exclusions are documented as deliberate.

Tests: 152 passed; two mutations verified red (nested refusal, trim).
This commit is contained in:
Shaojin Wen committed 2026-11-02 19:23:21 +08:00
1 parent 19b14b1a8f
commit fed6afb717
6 files changed
+80 -22

No files matched your search

+5 -3
View File
@@ -340,9 +340,11 @@ certify the round.
'
```
The scan names credential-shaped ignored files (nested ones included) while
leaving installed dependencies and build output alone. Exit 4 means this is
not a boundary: stop and run nothing else.
The scan names credential-shaped ignored files (nested ones included),
leaves installed dependencies and build output alone, and refuses a nested
git repository outright: git does not descend into it, so a credential
file inside one would otherwise pass silently. Exit 4 means this is not a
boundary: stop and run nothing else.
6. **Run the round inside**, installing the CLI into the fresh home because
the container is not root. Each `docker exec` is a new shell, so the
+5 -3
View File
@@ -97,9 +97,11 @@ the real key.
and the CLI's JSON coordinates.
- Credential scan: a nested ignored `.env`, `*.pem`, `credentials*`, `.npmrc`
or `.netrc` is refused; installed `node_modules`/`dist` are ignored at any
depth, including inside a workspace package; enumeration is path-limited so
a large ignored dependency tree is never buffered; a missing `--ignored`
mode or a dropped pattern turns its test red.
depth, including inside a workspace package, and their contents are a
documented exclusion; enumeration is path-limited so a large ignored
dependency tree is never buffered; a nested git repository is refused
because git does not descend into it; a missing `--ignored` mode or a
dropped pattern turns its test red.
- Checker: `--context` overrides the writable artifact copy, refuses an
unusable snapshot instead of falling back (with a usable writable copy
seeded), and keeps its existing exit codes.
@@ -35,7 +35,7 @@ CI 车道保留内联代理:那份副本运行在 base-pinned workflow 的 roo
## 验收
- 代理:bearer 拒绝、精确路径白名单、base path 前缀、真实 key 注入、chunked 请求体下 hop-by-hop 剥离(固定集合与 `Connection` 点名的头)、gzip 上游响应(头被剥离)、响应头前断连中止、下游暂停存活、慢但健康的流、上游重置存活、IPv6 监听格式化,以及 CLI 的 JSON 坐标。
- 凭据扫描:嵌套 ignored 的 `.env`、`*.pem`、`credentials*`、`.npmrc`、`.netrc` 被拒绝;`node_modules`/`dist` 在任意深度(含 workspace 包内)被忽略;枚举由 pathspec 限制,不会先缓冲整棵依赖树;去掉 `--ignored` 模式或删掉某条模式会让测试变红。
- 凭据扫描:嵌套 ignored 的 `.env`、`*.pem`、`credentials*`、`.npmrc`、`.netrc` 被拒绝;`node_modules`/`dist` 在任意深度(含 workspace 包内)被忽略,其内容属文档化取舍;枚举由 pathspec 限制,不会先缓冲整棵依赖树;嵌套 git 仓库被直接拒绝(git 不会进入其中);去掉 `--ignored` 模式或删掉某条模式会让测试变红。
- 检查器:`--context` 覆盖可写产物副本、不可用快照时拒绝而不是回退(产物副本先种一个合法 head),并保持既有退出码。
- 配方:每个脚本都点名可信 checkout、base 提交的 `.qwen` 只读挂载、两个凭据都来自文件而非命令行、导出 token 前等待代理坐标就绪、宣誓在轮次 shell 但不在 `docker run` 行;任一被还原都会让配方 pin 变红。
- 变异证据:移除上述任一守卫,对应 pin 必须变红。
@@ -144,6 +144,20 @@ describe('scanIgnoredCredentials', () => {
]);
});
it('refuses a nested repository instead of silently skipping it', () => {
const dir = makeRepo(['tmp/pr-clone/.env'], ['tmp/']);
execFileSync('git', ['init', '-q', '.'], {
cwd: join(dir, 'tmp', 'pr-clone'),
env: gitEnv,
});
expect(() => scanIgnoredCredentials(dir)).toThrow(
/nested git repository under tmp\/pr-clone/,
);
const result = run(dir);
expect(result.status).toBe(1);
expect(result.stderr).toContain('nested git repository');
});
it('stays silent on a clean tree', () => {
const dir = makeRepo(['src/app.ts'], ['node_modules/']);
const result = run(dir);
@@ -168,6 +182,8 @@ describe('scanIgnoredCredentials', () => {
tmpDirs.push(notRepo);
const result = run(notRepo);
expect(result.status).toBe(1);
expect(result.stderr).toContain('could not list ignored files');
expect(result.stderr).toMatch(
/could not list (nested repositories|ignored files)/,
);
});
});
+5 -1
View File
@@ -253,7 +253,7 @@ describe('verify-pr-model-proxy', () => {
method: 'POST',
headers: {
authorization: `Bearer ${TOKEN}`,
connection: 'x-client-hop',
connection: ' x-client-hop , keep-alive ',
'x-client-hop': 'client-secret',
'proxy-authorization': 'Basic client-proxy-secret',
'transfer-encoding': 'chunked',
@@ -622,6 +622,10 @@ describe('local container recipe pins', () => {
expect(skill).toContain('npm install -g --prefix "$HOME/.local" /work');
});
it('refuses a nested repository in the scan description', () => {
expect(skill).toMatch(/nested\s+git repository outright/);
});
it('pins the checker head from the host snapshot', () => {
expect(skill).toContain('--context ~/.cache/qwen-verify/pr<n>/pr.json');
});
+47 -13
View File
@@ -22,7 +22,10 @@
* The pathspecs both restrict enumeration to credential-shaped names and
* exclude those directories, so an installed tree's multi-megabyte ignored
* listing never reaches the output buffer. A credential file placed in the
* project (including a nested ignored directory) is still reported.
* project (including a nested ignored directory) is still reported. Two
* exclusions are deliberate: credentials inside `node_modules`/`dist` are
* accepted as installed/build content, and a nested git repository is refused
* outright because git will not descend into it — scan that clone separately.
*
* Usage:
* node scripts/verify-pr-credential-scan.mjs <root>
@@ -34,8 +37,6 @@ import { spawnSync } from 'node:child_process';
import { basename } from 'node:path';
import { isEntryPoint } from './lib/entry-point.js';
const EXCLUDED_SEGMENTS = new Set(['node_modules', 'dist', '.git']);
const CREDENTIAL_BASENAME =
/^(?:\.env(?:\..*)?|.*\.env|.*\.pem|.*\.key|credentials.*|\.npmrc|\.netrc)$/;
@@ -55,19 +56,21 @@ const CREDENTIAL_PATHSPECS = [
':(exclude,glob)**/dist/**',
];
// A nested clone is a repository git does not descend into, so a credential
// file inside it would silently pass an ignored-file scan. Dependencies and
// build output keep their own exclusion: a vendored `.git` is no more a
// project credential than a vendored `.env`.
const NESTED_REPO_PATHSPECS = [
':(glob)**/.git',
':(exclude,glob)**/node_modules/**',
':(exclude,glob)**/dist/**',
];
/** True for a path whose basename is credential-shaped. */
export function isCredentialShaped(path) {
return CREDENTIAL_BASENAME.test(basename(path));
}
/**
* True for a path under an installed/build directory at any depth, including
* a workspace package's own `node_modules` and `dist`.
*/
export function isExcluded(path) {
return path.split('/').some((segment) => EXCLUDED_SEGMENTS.has(segment));
}
export function gitScanArgs(root) {
return [
'-C',
@@ -82,15 +85,47 @@ export function gitScanArgs(root) {
];
}
/** Returns the nested repositories found under `root`, sorted. */
export function nestedGitRepos(root, { maxBuffer = 8 * 1024 * 1024 } = {}) {
const result = spawnSync(
'git',
[
'-C',
root,
'ls-files',
'--others',
'--ignored',
'--exclude-standard',
'-z',
'--',
...NESTED_REPO_PATHSPECS,
],
{ encoding: 'utf8', maxBuffer },
);
if (result.status !== 0) {
throw new Error(
`could not list nested repositories: ${(result.stderr || '').trim() || 'git failed'}`,
);
}
return result.stdout.split('\0').filter(Boolean).sort();
}
/**
* Returns the ignored credential-shaped files under `root`, sorted. An empty
* array is the clean state; a git failure is thrown so the caller can refuse
* rather than treat it as clean.
* rather than treat it as clean, and a nested repository is refused because a
* scan of the outer tree cannot see into it.
*/
export function scanIgnoredCredentials(
root,
{ maxBuffer = 8 * 1024 * 1024 } = {},
) {
const nested = nestedGitRepos(root, { maxBuffer });
if (nested.length > 0) {
throw new Error(
`nested git repository under ${nested[0]} — scan that clone separately or remove it`,
);
}
const result = spawnSync('git', gitScanArgs(root), {
encoding: 'utf8',
maxBuffer,
@@ -103,7 +138,6 @@ export function scanIgnoredCredentials(
return result.stdout
.split('\0')
.filter(Boolean)
.filter((path) => !isExcluded(path))
.filter((path) => isCredentialShaped(path))
.sort();
}