diff --git a/backend/src/bounties/linear-auth.test.ts b/backend/src/bounties/linear-auth.test.ts new file mode 100644 index 0000000..790fbfb --- /dev/null +++ b/backend/src/bounties/linear-auth.test.ts @@ -0,0 +1,27 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { maybeHandleBountyLinearComment } from "./webhooks.js"; +import type { Integration } from "../types.js"; + +test("maybeHandleBountyLinearComment rejects Linear bounty command from unauthorized non-privileged actor", () => { + const mockIntegration: Integration = { + id: "int_123", + userId: "user_owner_456", + provider: "linear", + tokens: { accessToken: "token_123" }, + createdAt: new Date(), + updatedAt: new Date(), + }; + + const unauthorizedEvent = { + data: { + body: "bounty $500 7d", + issueId: "issue_999", + user: { id: "guest_user_789", admin: false }, + }, + actor: { id: "guest_user_789", admin: false }, + }; + + const handled = maybeHandleBountyLinearComment(unauthorizedEvent, mockIntegration); + assert.equal(handled, false, "Unauthorized Linear bounty command must be rejected and return false"); +}); diff --git a/backend/src/bounties/webhooks.ts b/backend/src/bounties/webhooks.ts index 9a4bb95..b325b24 100644 --- a/backend/src/bounties/webhooks.ts +++ b/backend/src/bounties/webhooks.ts @@ -133,6 +133,15 @@ export function maybeHandleBountyComment(event: any): boolean { export function maybeHandleBountyLinearComment(event: any, integration: Integration): boolean { const body: string = event?.data?.body || ""; if (!looksLikeBountyCommand(body)) return false; + const isAuthorized = + event?.data?.user?.admin === true || + event?.actor?.admin === true || + event?.data?.user?.id === integration.userId || + event?.actor?.id === integration.userId; + if (!isAuthorized) { + console.warn(`[bounty] Linear bounty command rejected: unauthorized actor`); + return false; + } const cmd = parseBountyCommand(body); if (!cmd) return false; if (!isStellarConfigured()) {