fix(prd-7): M3 security review — self-trigger guard, actor permissions, trigger bypass, platform validation

- Add self-mention/bot loop prevention in actor.ts (blocks actor matching bot_name)
- Switch Forgejo permission check to actor-level via getCollaboratorPermissionLevel
  with fallback to token-level repos.get() when collaborator API returns 403
- Close empty-body PR review comment trigger bypass (now returns false instead of true)
- Add platform detection cross-validation (reject PLATFORM=forgejo on github.com)
- Document URL builder defense-in-depth rationale in comment-logic.ts
- Update/add tests across 6 test files (actor, permissions, trigger, platform, mcp, m7)

Progress: 50% complete (3/6 milestones) — next: M4 test coverage review

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vlad MocanuandClaude Opus 4.6 committed 2026-02-26 12:31:12 +02:00
1 parent cefc229199
commit 6b0302d226
12 files changed
+270 -68

No files matched your search

+37 -2
View File
@@ -121,6 +121,8 @@ describe("checkHumanActor", () => {
savedPlatform = process.env.PLATFORM;
savedServerUrl = process.env.GITHUB_SERVER_URL;
process.env.PLATFORM = "forgejo";
// Ensure cross-validation doesn't reject PLATFORM=forgejo
process.env.GITHUB_SERVER_URL = "https://git.example.com";
});
afterEach(() => {
@@ -132,7 +134,7 @@ describe("checkHumanActor", () => {
_resetPlatformCache();
});
test("should skip actor check entirely on Forgejo", async () => {
test("should skip GitHub API actor check on Forgejo for regular users", async () => {
let apiCalled = false;
const mockOctokit = {
users: {
@@ -151,7 +153,7 @@ describe("checkHumanActor", () => {
expect(apiCalled).toBe(false);
});
test("should skip actor check for bot-like names on Forgejo", async () => {
test("should skip actor check for bot-like names that don't match bot_name", async () => {
let apiCalled = false;
const mockOctokit = {
users: {
@@ -170,5 +172,38 @@ describe("checkHumanActor", () => {
).resolves.toBeUndefined();
expect(apiCalled).toBe(false);
});
test("should block self-trigger when actor matches bot_name (with [bot] suffix)", async () => {
const mockOctokit = createMockOctokit("User");
const context = createMockContext();
// Default botName is "claude[bot]" from CLAUDE_BOT_LOGIN
context.actor = "claude[bot]";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Blocking self-trigger on Forgejo",
);
});
test("should block self-trigger when actor matches bot_name without [bot] suffix", async () => {
const mockOctokit = createMockOctokit("User");
const context = createMockContext();
context.inputs.botName = "claude[bot]";
context.actor = "claude";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Blocking self-trigger on Forgejo",
);
});
test("should block self-trigger with custom bot_name", async () => {
const mockOctokit = createMockOctokit("User");
const context = createMockContext();
context.inputs.botName = "my-assistant";
context.actor = "my-assistant";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Blocking self-trigger on Forgejo",
);
});
});
});