fix(ui): confirm a Slack install only from a readable integration

- Validate the exchange body as a minimal JSON:API resource before the cast
- Report truthy-but-unreadable payloads as the existing unconfirmed result
This commit is contained in:
Pablo F.G
2026-08-18 09:23:40 +02:00
parent 5ed235088c
commit 68f1092f56
2 changed files with 75 additions and 1 deletions
+54
View File
@@ -5,6 +5,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { SLACK_UNREADABLE_RESULT_MESSAGE } from "@/lib/integrations/slack-errors";
import { SentryErrorSource, SentryErrorType } from "@/sentry";
const { captureExceptionMock, captureMessageMock, fetchMock } = vi.hoisted(
@@ -221,3 +222,56 @@ describe("getSlackAuthorizeUrl authorize URL", () => {
});
});
});
/**
* The callback names the workspace and redirects on `integration` alone, so a
* `2xx` body it cannot read back as an integration must not reach it.
*/
describe("exchangeSlackOAuthCode result shape", () => {
const INTEGRATION = {
id: "9b1f4c22-5e7a-4c2e-8f0d-6a3b1c9d7e42",
type: "integrations",
attributes: {
integration_type: "slack",
configuration: { team_name: "Prowler HQ" },
},
};
const exchangeResponse = (data: unknown) =>
new Response(JSON.stringify({ data }), {
status: 200,
headers: { "content-type": "application/vnd.api+json" },
});
it.each<[string, unknown]>([
["an empty object", {}],
["an array", []],
["a bare string", "invalid"],
["a resource with no id", { type: "integrations", attributes: {} }],
[
"a resource with no attributes",
{ id: INTEGRATION.id, type: "integrations" },
],
])("cannot confirm the install from %s", async (_label, data) => {
// Given — a 2xx whose `data` is truthy but is not an integration resource.
fetchMock.mockResolvedValue(exchangeResponse(data));
// When
const result = await exchange();
// Then — the answer for a body with no `data`: the install happened, only
// its result is unknown.
expect(result).toEqual({
unconfirmed: true,
message: SLACK_UNREADABLE_RESULT_MESSAGE,
});
});
it("hands over the workspace the API upserted", async () => {
// Given
fetchMock.mockResolvedValue(exchangeResponse(INTEGRATION));
// When / Then
expect(await exchange()).toEqual({ integration: INTEGRATION });
});
});
+21 -1
View File
@@ -98,6 +98,26 @@ const isSlackAuthorizeUrl = (value: string): boolean => {
}
};
/**
* The callback names the workspace and redirects on this value alone, so a `2xx`
* payload that is not a JSON:API resource (`{}`, `[]`, `"invalid"`) must read as
* unreadable rather than as a connected workspace.
*/
const isIntegrationResource = (value: unknown): boolean => {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return false;
}
const { id, attributes } = value as Record<string, unknown>;
return (
typeof id === "string" &&
typeof attributes === "object" &&
attributes !== null &&
!Array.isArray(attributes)
);
};
const failureFrom = async (
response: Response,
fallback: string,
@@ -205,7 +225,7 @@ export const exchangeSlackOAuthCode = async (
revalidatePath("/integrations");
revalidatePath("/integrations/slack");
if (!body?.data) {
if (!isIntegrationResource(body?.data)) {
return { unconfirmed: true, message: SLACK_UNREADABLE_RESULT_MESSAGE };
}