From b573b4d46f285e840eefab940919bb97a858e048 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 15 Jul 2026 10:32:42 +0200 Subject: [PATCH] fix(custom-widgets): parse fenced imports safely --- packages/custom-widgets/src/core/import.ts | 28 +++++++++- .../custom-widgets/src/test/import.spec.ts | 12 +++++ pnpm-lock.yaml | 53 +++++++++---------- 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/packages/custom-widgets/src/core/import.ts b/packages/custom-widgets/src/core/import.ts index 36c40fb06..e325bbe35 100644 --- a/packages/custom-widgets/src/core/import.ts +++ b/packages/custom-widgets/src/core/import.ts @@ -8,10 +8,34 @@ export interface ImportReview { hasActions: boolean; } +function extractFencedBlock(text: string, acceptedLanguages: readonly string[]): string | undefined { + let cursor = 0; + + while (cursor < text.length) { + const fenceStart = text.indexOf("```", cursor); + if (fenceStart === -1) return undefined; + + const headerStart = fenceStart + 3; + const headerEnd = text.indexOf("\n", headerStart); + if (headerEnd === -1) return undefined; + + const contentStart = headerEnd + 1; + const fenceEnd = text.indexOf("```", contentStart); + if (fenceEnd === -1) return undefined; + + const language = text.slice(headerStart, headerEnd).trim().toLowerCase(); + if (acceptedLanguages.includes(language)) return text.slice(contentStart, fenceEnd); + + cursor = fenceEnd + 3; + } + + return undefined; +} + export function parseCustomWidgetClipboard(text: string): Record | null { try { - const jsonBlock = text.match(/```(?:json)?\s*([\s\S]*?)```/iu)?.[1]; - const jsxBlock = text.match(/```(?:jsx|tsx)\s*([\s\S]*?)```/iu)?.[1]?.trim(); + const jsonBlock = extractFencedBlock(text, ["", "json"]); + const jsxBlock = extractFencedBlock(text, ["jsx", "tsx"])?.trim(); const value: unknown = JSON.parse(jsonBlock ?? text); if (!value || typeof value !== "object" || Array.isArray(value)) return null; const widget = value as Record; diff --git a/packages/custom-widgets/src/test/import.spec.ts b/packages/custom-widgets/src/test/import.spec.ts index a2db69708..eb9d130e1 100644 --- a/packages/custom-widgets/src/test/import.spec.ts +++ b/packages/custom-widgets/src/test/import.spec.ts @@ -53,4 +53,16 @@ ${JSON.stringify(widget, null, 2)} '\n {data.name}\n', ); }); + + it("accepts case-insensitive fenced languages and CRLF line endings", () => { + const result = parseCustomWidgetClipboard( + `\`\`\`JSON\r\n${JSON.stringify(widget)}\r\n\`\`\`\r\n\`\`\`TSX\r\n{data.name}\r\n\`\`\``, + ); + + expect((result?.displayConfig as Record | undefined)?.template).toBe("{data.name}"); + }); + + it("rejects an unterminated fence with a large whitespace payload", () => { + expect(parseCustomWidgetClipboard(`\`\`\`json\n${" ".repeat(250_000)}`)).toBeNull(); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34815431a..a4accb126 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1755,7 +1755,7 @@ importers: version: link:../../tooling/typescript '@types/node': specifier: 'catalog:' - version: 24.13.2 + version: 24.13.3 '@types/react': specifier: 'catalog:' version: 19.2.17 @@ -1773,7 +1773,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.2)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.3)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) packages/db: dependencies: @@ -18918,7 +18918,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -20501,7 +20501,7 @@ snapshots: '@scalar/icons@0.7.3(typescript@6.0.3)': dependencies: '@phosphor-icons/core': 2.1.1 - '@types/node': 24.13.2 + '@types/node': 24.13.3 chalk: 5.6.2 vue: 3.5.35(typescript@6.0.3) transitivePeerDependencies: @@ -21476,11 +21476,11 @@ snapshots: '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/bonjour@3.5.13': dependencies: - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/chai@5.2.2': dependencies: @@ -21491,11 +21491,11 @@ snapshots: '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 4.19.5 - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/connect@3.4.38': dependencies: - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/cookies@0.9.2': dependencies: @@ -21641,13 +21641,13 @@ snapshots: '@types/docker-modem@3.0.6': dependencies: - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/ssh2': 1.15.1 '@types/dockerode@4.0.1': dependencies: '@types/docker-modem': 3.0.6 - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/ssh2': 1.15.1 '@types/estree-jsx@1.0.5': @@ -21658,7 +21658,7 @@ snapshots: '@types/express-serve-static-core@4.19.5': dependencies: - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/qs': 6.9.16 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -21690,7 +21690,7 @@ snapshots: '@types/http-proxy@1.17.17': dependencies: - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/istanbul-lib-coverage@2.0.6': {} @@ -21720,7 +21720,7 @@ snapshots: '@types/node-fetch@2.6.13': dependencies: - '@types/node': 24.13.2 + '@types/node': 24.13.3 form-data: 4.0.6 '@types/node-unifi@2.5.1(patch_hash=5e6ae51e2a17a7f9729bfa30b0eb3d0842a5810ac6db47603ab4a6efa1ed84c5)': @@ -21784,14 +21784,14 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/semver@7.5.8': {} '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/serve-index@1.9.4': dependencies: @@ -21800,7 +21800,7 @@ snapshots: '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/send': 0.17.4 '@types/set-cookie-parser@2.4.10': @@ -21810,15 +21810,15 @@ snapshots: '@types/sockjs@0.3.36': dependencies: - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/ssh2-streams@0.1.12': dependencies: - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/ssh2@0.5.52': dependencies: - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/ssh2-streams': 0.1.12 '@types/ssh2@1.15.1': @@ -21830,7 +21830,7 @@ snapshots: '@types/stream-buffers@3.0.7': dependencies: - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/triple-beam@1.3.5': {} @@ -21855,7 +21855,7 @@ snapshots: '@types/xml2js@0.4.14': dependencies: - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/yargs-parser@21.0.3': {} @@ -21979,7 +21979,6 @@ snapshots: optionalDependencies: msw: 2.14.6(@types/node@24.13.3)(typescript@6.0.3) vite: 8.1.3(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) - optional: true '@vitest/pretty-format@4.1.9': dependencies: @@ -24144,7 +24143,7 @@ snapshots: eval@0.1.8: dependencies: - '@types/node': 24.13.2 + '@types/node': 24.13.3 require-like: 0.1.2 event-target-shim@5.0.1: {} @@ -25450,7 +25449,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 24.13.2 + '@types/node': 24.13.3 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -25464,7 +25463,7 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 24.13.2 + '@types/node': 24.13.3 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -27867,7 +27866,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 24.13.2 + '@types/node': 24.13.3 long: 5.3.2 proxmox-api@1.1.1: @@ -29874,7 +29873,6 @@ snapshots: terser: 5.46.1 tsx: 4.21.0 yaml: 2.9.0 - optional: true vitest@4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.3)(typescript@6.0.3))(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)): dependencies: @@ -29937,7 +29935,6 @@ snapshots: jsdom: 29.1.1(@noble/hashes@2.0.1) transitivePeerDependencies: - msw - optional: true vue-component-type-helpers@3.3.4: {}