History

Guard generic OpenAPI imports by size

Rhys Sullivan committed 4 months ago

Commit 82561081 parent f80115d3 files · +54 −5Browse files

Unified · Split

3 files
  1. packages/plugins/openapi/src/sdk/parse.test.ts +19 −1
  2. packages/plugins/openapi/src/sdk/parse.ts +32 −2
  3. packages/plugins/openapi/src/sdk/plugin.ts +3 −2
packages/plugins/openapi/src/sdk/parse.test.ts+19−1
@@ -2,7 +2,11 @@ import { describe, expect, it } from "@effect/vitest"; · 1 unmodified line
22 import { Effect } from "effect";
33
44 import { OpenApiParseError } from "./errors";
5−import { parse } from "./parse";
5+import {
6+ ensureGenericOpenApiSpecTextWithinLimit,
7+ MAX_GENERIC_OPENAPI_SPEC_TEXT_LENGTH,
8+ parse,
9+} from "./parse";
610
711 describe("OpenAPI parse", () => {
812 it.effect("parses JSON OpenAPI documents", () =>
@@ -64,4 +68,18 @@ paths: {} · 55 unmodified lines
6468 expect(error).toHaveProperty("message", "OpenAPI document must parse to an object");
6569 }),
6670 );
71+
72+ it.effect("rejects oversized generic OpenAPI documents before parsing", () =>
73+ Effect.gen(function* () {
74+ const error = yield* ensureGenericOpenApiSpecTextWithinLimit(
75+ "x".repeat(MAX_GENERIC_OPENAPI_SPEC_TEXT_LENGTH + 1),
76+ ).pipe(Effect.flip);
77+
78+ expect(error).toBeInstanceOf(OpenApiParseError);
79+ expect(error).toHaveProperty(
80+ "message",
81+ expect.stringContaining("too large for the generic OpenAPI importer"),
82+ );
83+ }),
84+ );
6785 });
packages/plugins/openapi/src/sdk/parse.ts+32−2
@@ -12,6 +12,31 @@ export interface SpecFetchCredentials { · 11 unmodified lines
1212 readonly queryParams?: Record<string, string>;
1313 }
1414
15+export const MAX_GENERIC_OPENAPI_SPEC_TEXT_LENGTH = 25 * 1024 * 1024;
16+
17+const formatMiB = (bytes: number): string => `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
18+
19+const oversizedOpenApiSpecError = (size: number): OpenApiParseError =>
20+ new OpenApiParseError({
21+ message: `OpenAPI document is too large for the generic OpenAPI importer (${formatMiB(size)}; limit ${formatMiB(MAX_GENERIC_OPENAPI_SPEC_TEXT_LENGTH)}). Use a provider-specific integration when available, or import a smaller filtered OpenAPI document.`,
22+ });
23+
24+const parseContentLength = (
25+ headers: Readonly<Record<string, string | undefined>>,
26+): number | undefined => {
27+ const raw = headers["content-length"] ?? headers["Content-Length"];
28+ if (!raw) return undefined;
29+ const parsed = Number(raw);
30+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
31+};
32+
33+export const ensureGenericOpenApiSpecTextWithinLimit = (
34+ specText: string,
35+): Effect.Effect<string, OpenApiParseError> =>
36+ specText.length > MAX_GENERIC_OPENAPI_SPEC_TEXT_LENGTH
37+ ? Effect.fail(oversizedOpenApiSpecError(specText.length))
38+ : Effect.succeed(specText);
39+
1540 // ExtractionError subclass raised from parse() for non-3.x specs
1641 class OpenApiExtractionErrorFromParse extends OpenApiExtractionError {}
1742
@@ -50,7 +75,11 @@ export const fetchSpecText = Effect.fn("OpenApi.fetchSpecText")(function* ( · 32 unmodified lines
5075 message: `Failed to fetch OpenAPI document: HTTP ${response.status}`,
5176 });
5277 }
53− return yield* response.text.pipe(
78+ const contentLength = parseContentLength(response.headers);
79+ if (contentLength !== undefined && contentLength > MAX_GENERIC_OPENAPI_SPEC_TEXT_LENGTH) {
80+ return yield* oversizedOpenApiSpecError(contentLength);
81+ }
82+ const specText = yield* response.text.pipe(
5483 Effect.mapError(
5584 (_cause) =>
5685 new OpenApiParseError({
@@ -58,6 +87,7 @@ export const fetchSpecText = Effect.fn("OpenApi.fetchSpecText")(function* ( · 1 unmodified line
5887 }),
5988 ),
6089 );
90+ return yield* ensureGenericOpenApiSpecTextWithinLimit(specText);
6191 });
6292
6393 /**
@@ -67,7 +97,7 @@ export const fetchSpecText = Effect.fn("OpenApi.fetchSpecText")(function* ( · 3 unmodified lines
6797 export const resolveSpecText = (input: string, credentials?: SpecFetchCredentials) =>
6898 input.startsWith("http://") || input.startsWith("https://")
6999 ? fetchSpecText(input, credentials)
70− : Effect.succeed(input);
100+ : ensureGenericOpenApiSpecTextWithinLimit(input);
71101
72102 /**
73103 * Parse an OpenAPI document from spec text and validate it's OpenAPI 3.x.
packages/plugins/openapi/src/sdk/plugin.ts+3−2
@@ -22,7 +22,7 @@ import { · 21 unmodified lines
2222
2323 import { decodeOpenApiIntegrationConfig, type OpenApiIntegrationConfig } from "./config";
2424 import { OpenApiExtractionError, OpenApiOAuthError, OpenApiParseError } from "./errors";
25−import { parse, resolveSpecText } from "./parse";
25+import { ensureGenericOpenApiSpecTextWithinLimit, parse, resolveSpecText } from "./parse";
2626 import {
2727 convertGoogleDiscoveryBundleToOpenApi,
2828 convertGoogleDiscoveryToOpenApi,
@@ -473,7 +473,8 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 444 unmodified lines
473473 const specText = yield* resolveSpecText(spec.url).pipe(Effect.provide(httpClientLayer));
474474 return { specText };
475475 }
476− return { specText: spec.value };
476+ const specText = yield* ensureGenericOpenApiSpecTextWithinLimit(spec.value);
477+ return { specText };
477478 });
478479
479480 return {