diff --git a/docs/operator-guide.zh-CN.md b/docs/operator-guide.zh-CN.md index 87e2f87..7b9d0b7 100644 --- a/docs/operator-guide.zh-CN.md +++ b/docs/operator-guide.zh-CN.md @@ -166,6 +166,11 @@ image;项目安全设置只能收紧用户限额和默认禁止路径,不能 未跟踪文件、符号链接、`.env` 文件、仓库根级 `secrets`/`credentials` 前缀和包含通配符或父目录 跳转的路径仍会失败关闭。每个授权前缀都会写入验证沙箱清单,便于 Review 时核对。 +已跟踪的 `.env.example`、`.env.sample` 和 `.env.template` 可以进入验证快照,但敏感字段只允许 +空值或严格的占位符:`replace-with-<名称>`、`your-<提供方>-api-key`。名称和提供方仅允许小写 +ASCII 字母、数字及分隔非空片段的连字符;整个占位符不能加引号且最多 128 字符。模板仍须满足 +文件大小、UTF-8、普通文件、目录和赋值语法检查;其他非空敏感值继续拒绝,错误只报告位置。 + ### 并发与变更规模 默认情况下,每个仓库最多同时保留 4 个由当前 GitHub 账号创建的 open PR;Draft 和 Ready 都计入, diff --git a/src/reposteward/verifier.py b/src/reposteward/verifier.py index 1eb72e5..49ee7d4 100644 --- a/src/reposteward/verifier.py +++ b/src/reposteward/verifier.py @@ -57,7 +57,10 @@ re.IGNORECASE, ) EMPTY_ENV_VALUE = re.compile(r"(?:|''|\"\")(?:\s+#.*)?") -SAFE_ENV_PLACEHOLDER = re.compile(r"replace-with-[a-z0-9]+(?:-[a-z0-9]+)*") +SAFE_ENV_PLACEHOLDER = re.compile( + r"(?:replace-with-[a-z0-9]+(?:-[a-z0-9]+)*" + r"|your-[a-z0-9]+(?:-[a-z0-9]+)*-api-key)" +) class VerificationError(RuntimeError): diff --git a/tests/test_verifier.py b/tests/test_verifier.py index bb56fd5..3228e19 100644 --- a/tests/test_verifier.py +++ b/tests/test_verifier.py @@ -378,6 +378,37 @@ def test_tracked_environment_template_accepts_explicit_placeholders(self) -> Non template.read_text(encoding="utf-8"), ) + def test_tracked_template_accepts_api_key_placeholders(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + worktree = root / "worktree" + worktree.mkdir() + _repository(worktree) + values = ( + "your-serper-api-key", + "your-serply-api-key", + "your-tavily-api-key", + "your-jina-api-key", + "your-infoquest-api-key", + "your-sofya-api-key", + "your-provider-v2-api-key", + "your-" + "a" * 115 + "-api-key", + ) + content = "".join( + f"PROVIDER_{index}_API_KEY={value}\n" + for index, value in enumerate(values) + ) + template = worktree / ".env.example" + template.write_text(content, encoding="utf-8") + subprocess.run(["git", "add", ".env.example"], cwd=worktree, check=True) + + DockerVerifier._copy_workspace(worktree, root / "snapshot") + + self.assertEqual( + (root / "snapshot" / ".env.example").read_bytes(), + template.read_bytes(), + ) + def test_untracked_environment_template_is_excluded(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) @@ -414,6 +445,18 @@ def test_environment_template_rejects_invalid_sensitive_placeholders(self) -> No "replace-with-token.value", '"replace-with-a-token"', "replace-with-" + "a" * 129, + "your-api-key", + "your--api-key", + "your-UPPER-api-key", + "your-provider_api-key", + "your-provider--v2-api-key", + "your-provider-api-key.value", + "your-provider-api-key-extra", + '"your-provider-api-key"', + "'your-provider-api-key'", + "your-provider-api-key$(id)", + "your-供应商-api-key", + "your-" + "a" * 116 + "-api-key", ) ): with self.subTest(value=value):