Skip to content

Commit fc7df9c

Browse files
fix(github): remove raw webhook payload logging (#500)
- Remove raw GitHub webhook body previews from adapter debug/error logs - Prevents webhook payload content from being copied into application logs when debug logging is enabled - No webhook routing or response behavior change; only log fields change ## Context The GitHub adapter logged a preview of incoming webhook request bodies while handling webhooks. Raw webhook payloads can contain repository metadata, user-authored issue or pull request text, URLs, installation details, and other provider-controlled content. Even at debug level, SDK logging should avoid copying raw provider payloads into application logs by default. ## Problem Debug logging should provide useful operational context without changing the privacy boundary of webhook data. The previous log emitted a raw body preview before signature verification. That meant an application with debug logging enabled could record payload content from both valid GitHub webhook events and invalid requests that were later rejected. This is unnecessary for normal webhook troubleshooting. Derived request metadata is enough to understand routing and parsing failures without retaining payload text. ## Changes The GitHub adapter no longer logs raw webhook bodies or body previews. Webhook logs now use bounded request-shape metadata: - `bodyBytes` - `contentType` - `eventType` - `signaturePresent` - `jsonParseStatus` for invalid JSON The change preserves signature verification, ping handling, JSON parsing, and event routing behavior. Regression tests cover invalid signature, invalid JSON, and valid webhook paths with token-shaped and customer-slug sentinel strings in the payload. The tests assert those sentinels, the full raw body, the old raw-body log message, and `bodyPreview` do not appear in logger calls. A patch changeset is included for `@chat-adapter/github`. ## Verification - `pnpm turbo build --filter @chat-adapter/github` - `pnpm --filter @chat-adapter/github test` - `pnpm --filter @chat-adapter/github typecheck` - `pnpm check` - `git diff --check`
1 parent 0d4e3ee commit fc7df9c

3 files changed

Lines changed: 168 additions & 9 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@chat-adapter/github": patch
3+
---
4+
5+
Remove raw GitHub webhook payload previews from adapter logs.
6+
7+
Debug and error logs now report only request-shape metadata, such as body size, event type, content type, and signature presence, instead of copying provider payload content into logs.

packages/adapter-github/src/index.test.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ const mockLogger = {
9090

9191
const WEBHOOK_SECRET = "test-secret";
9292
const INSTALLATION_ERROR_PATTERN = /installation/i;
93+
const WEBHOOK_LOG_SENTINELS = [
94+
"secret-access-token",
95+
"secret-refresh-token",
96+
"Bearer secret-token",
97+
"Authorization",
98+
"customer-team-slug",
99+
"access_token",
100+
"refresh_token",
101+
] as const;
93102

94103
function signPayload(body: string): string {
95104
return `sha256=${createHmac("sha256", WEBHOOK_SECRET).update(body).digest("hex")}`;
@@ -193,6 +202,22 @@ function createMockState() {
193202
};
194203
}
195204

205+
function stringifyLoggerCalls(): string {
206+
return JSON.stringify({
207+
debug: mockLogger.debug.mock.calls,
208+
error: mockLogger.error.mock.calls,
209+
info: mockLogger.info.mock.calls,
210+
warn: mockLogger.warn.mock.calls,
211+
});
212+
}
213+
214+
function findLoggedSentinels(): string[] {
215+
const logCalls = stringifyLoggerCalls();
216+
return WEBHOOK_LOG_SENTINELS.filter((sentinel) =>
217+
logCalls.includes(sentinel)
218+
);
219+
}
220+
196221
// ─── Tests ───────────────────────────────────────────────────────────────────
197222

198223
describe("GitHubAdapter", () => {
@@ -596,6 +621,43 @@ describe("GitHubAdapter", () => {
596621
expect(response.status).toBe(401);
597622
});
598623

624+
it("should not log raw payload content for invalid signatures", async () => {
625+
const body = JSON.stringify({
626+
action: "created",
627+
access_token: "secret-access-token",
628+
authorization: "Bearer secret-token",
629+
comment: {
630+
body: "Authorization: Bearer secret-token",
631+
},
632+
refresh_token: "secret-refresh-token",
633+
repository: {
634+
full_name: "customer-team-slug/app",
635+
owner: { login: "customer-team-slug" },
636+
},
637+
});
638+
const request = makeWebhookRequest(
639+
body,
640+
"issue_comment",
641+
"sha256=invalid"
642+
);
643+
644+
const response = await adapter.handleWebhook(request);
645+
646+
expect(response.status).toBe(401);
647+
expect(mockLogger.debug).toHaveBeenCalledWith(
648+
"GitHub webhook signature verification failed",
649+
{
650+
bodyBytes: Buffer.byteLength(body, "utf8"),
651+
contentType: "application/json",
652+
eventType: "issue_comment",
653+
signaturePresent: true,
654+
}
655+
);
656+
expect(findLoggedSentinels()).toEqual([]);
657+
expect(stringifyLoggerCalls()).not.toContain(body);
658+
expect(stringifyLoggerCalls()).not.toContain("GitHub webhook raw body");
659+
});
660+
599661
it("should return 200 pong for ping event", async () => {
600662
const body = JSON.stringify({ zen: "test" });
601663
const signature = signPayload(body);
@@ -617,6 +679,90 @@ describe("GitHubAdapter", () => {
617679
expect(text).toContain("Invalid JSON");
618680
});
619681

682+
it("should not log raw payload content for invalid JSON", async () => {
683+
const body =
684+
"not-json secret-access-token secret-refresh-token Bearer secret-token Authorization customer-team-slug access_token refresh_token";
685+
const signature = signPayload(body);
686+
const request = makeWebhookRequest(body, "issue_comment", signature);
687+
688+
const response = await adapter.handleWebhook(request);
689+
690+
expect(response.status).toBe(400);
691+
expect(mockLogger.error).toHaveBeenCalledWith(
692+
"GitHub webhook invalid JSON",
693+
{
694+
bodyBytes: Buffer.byteLength(body, "utf8"),
695+
contentType: "application/json",
696+
eventType: "issue_comment",
697+
jsonParseStatus: "error",
698+
signaturePresent: true,
699+
}
700+
);
701+
expect(findLoggedSentinels()).toEqual([]);
702+
expect(stringifyLoggerCalls()).not.toContain(body);
703+
expect(stringifyLoggerCalls()).not.toContain("bodyPreview");
704+
});
705+
706+
it("should not log raw payload content for valid webhooks", async () => {
707+
const mockChat = {
708+
getLogger: vi.fn(),
709+
getState: vi.fn(),
710+
getUserName: vi.fn(),
711+
handleIncomingMessage: vi.fn(),
712+
processMessage: vi.fn(),
713+
};
714+
mockUsersGetAuthenticated.mockResolvedValueOnce({
715+
data: { id: 777, login: "test-bot" },
716+
});
717+
await adapter.initialize(mockChat);
718+
719+
const basePayload = makeIssueCommentPayload();
720+
const payload = {
721+
...makeIssueCommentPayload({
722+
comment: {
723+
...basePayload.comment,
724+
body: "Authorization: Bearer secret-token secret-access-token secret-refresh-token",
725+
},
726+
repository: {
727+
...basePayload.repository,
728+
full_name: "customer-team-slug/app",
729+
owner: {
730+
...basePayload.repository.owner,
731+
login: "customer-team-slug",
732+
},
733+
},
734+
}),
735+
access_token: "secret-access-token",
736+
authorization: "Bearer secret-token",
737+
refresh_token: "secret-refresh-token",
738+
};
739+
const body = JSON.stringify(payload);
740+
const signature = signPayload(body);
741+
const request = makeWebhookRequest(body, "issue_comment", signature);
742+
743+
const response = await adapter.handleWebhook(request);
744+
745+
expect(response.status).toBe(200);
746+
expect(mockChat.processMessage).toHaveBeenCalledWith(
747+
adapter,
748+
"github:customer-team-slug/app:42",
749+
expect.objectContaining({ id: "100" }),
750+
undefined
751+
);
752+
expect(mockLogger.debug).toHaveBeenCalledWith(
753+
"GitHub webhook request verified",
754+
{
755+
bodyBytes: Buffer.byteLength(body, "utf8"),
756+
contentType: "application/json",
757+
eventType: "issue_comment",
758+
signaturePresent: true,
759+
}
760+
);
761+
expect(findLoggedSentinels()).toEqual([]);
762+
expect(stringifyLoggerCalls()).not.toContain(body);
763+
expect(stringifyLoggerCalls()).not.toContain("GitHub webhook raw body");
764+
});
765+
620766
it("should process issue_comment on PR with valid signature", async () => {
621767
const mockChat = {
622768
getLogger: vi.fn(),

packages/adapter-github/src/index.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -507,19 +507,25 @@ export class GitHubAdapter
507507
options?: WebhookOptions
508508
): Promise<Response> {
509509
const body = await request.text();
510-
this.logger.debug("GitHub webhook raw body", {
511-
body: body.substring(0, 500),
512-
});
510+
const signature = request.headers.get("x-hub-signature-256");
511+
const eventType = request.headers.get("x-github-event");
512+
const webhookMetadata = {
513+
bodyBytes: Buffer.byteLength(body, "utf8"),
514+
contentType: request.headers.get("content-type"),
515+
eventType,
516+
signaturePresent: signature !== null,
517+
};
513518

514519
// Verify request signature
515-
const signature = request.headers.get("x-hub-signature-256");
516520
if (!this.verifySignature(body, signature)) {
521+
this.logger.debug(
522+
"GitHub webhook signature verification failed",
523+
webhookMetadata
524+
);
517525
return new Response("Invalid signature", { status: 401 });
518526
}
519527

520-
// Get event type from header
521-
const eventType = request.headers.get("x-github-event");
522-
this.logger.debug("GitHub webhook event type", { eventType });
528+
this.logger.debug("GitHub webhook request verified", webhookMetadata);
523529

524530
// Handle ping event (webhook verification)
525531
if (eventType === "ping") {
@@ -535,8 +541,8 @@ export class GitHubAdapter
535541
payload = JSON.parse(body);
536542
} catch {
537543
this.logger.error("GitHub webhook invalid JSON", {
538-
contentType: request.headers.get("content-type"),
539-
bodyPreview: body.substring(0, 200),
544+
...webhookMetadata,
545+
jsonParseStatus: "error",
540546
});
541547
return new Response(
542548
"Invalid JSON. Make sure webhook Content-Type is set to application/json",

0 commit comments

Comments
 (0)