From 32ed0dbe408fffa09efc457e4cfc597af30f7e97 Mon Sep 17 00:00:00 2001 From: equationalapplications Date: Thu, 25 Jun 2026 15:40:10 -0400 Subject: [PATCH 01/11] fix(grounding): contain search suggestions WebView on iOS and Android Grounding metadata overlapped adjacent chat bubbles on native because WebViews in GiftedChat's inverted list paint outside their bounds. Clip the widget, auto-size to content, and disable FlatList subview clipping. Co-authored-by: Cursor --- src/components/ChatView.tsx | 9 ++- src/components/GroundingHtml.tsx | 135 +++++++++++++++++++++++++------ 2 files changed, 120 insertions(+), 24 deletions(-) diff --git a/src/components/ChatView.tsx b/src/components/ChatView.tsx index 2ed4f9a9..d4ee226d 100644 --- a/src/components/ChatView.tsx +++ b/src/components/ChatView.tsx @@ -4,6 +4,7 @@ import { useNavigation } from 'expo-router/react-navigation' import { View, Text as RNText, StyleSheet, Platform, TouchableOpacity, Linking } from 'react-native' import { GiftedChat, Bubble, InputToolbar, Send, MessageText } from 'react-native-gifted-chat' import type { IMessage, User, ComposerProps, SendProps, InputToolbarProps, MessageTextProps } from 'react-native-gifted-chat' +import type { ListViewProps } from 'react-native-gifted-chat/lib/MessageContainer/types' import { useSelector } from '@xstate/react' import { useCharacter } from '~/hooks/useCharacters' import { useAIChat } from '~/hooks/useAIChat' @@ -20,6 +21,9 @@ import type { Character } from '~/services/characterService' const defaultAvatarUrl = 'https://via.placeholder.com/150' +/** WebViews in inverted lists can paint over sibling rows unless clipping is disabled. */ +const groundingListViewProps = { removeClippedSubviews: false } as ListViewProps + interface ChatViewProps { characterId: string } @@ -332,6 +336,7 @@ function ChatViewContent({ renderCustomView={renderCustomView} isCustomViewBottom messageIdGenerator={() => `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`} + listViewProps={groundingListViewProps} renderAvatarOnTop messagesContainerStyle={styles.messagesContainer} minInputToolbarHeight={56} @@ -447,6 +452,8 @@ const styles = StyleSheet.create({ paddingHorizontal: 8, paddingBottom: 8, gap: 6, + overflow: 'hidden', + width: '100%', }, citationRow: { flexDirection: 'row', @@ -464,7 +471,7 @@ const styles = StyleSheet.create({ fontSize: 12, }, searchSuggestions: { - height: 85, + minHeight: 48, backgroundColor: 'transparent', }, headerTitle: { diff --git a/src/components/GroundingHtml.tsx b/src/components/GroundingHtml.tsx index 5894ed5c..fce5c4e8 100644 --- a/src/components/GroundingHtml.tsx +++ b/src/components/GroundingHtml.tsx @@ -1,4 +1,5 @@ -import { Linking } from 'react-native' +import { useMemo, useState } from 'react' +import { Linking, StyleSheet, View } from 'react-native' import type { StyleProp, ViewStyle } from 'react-native' import { WebView } from 'react-native-webview' import { isSafeHttpUrl } from '~/utils/isSafeHttpUrl' @@ -9,35 +10,123 @@ interface GroundingHtmlProps { style?: StyleProp } +const DEFAULT_MIN_HEIGHT = 48 +const MAX_HEIGHT = 160 + /** - * Native renderer for the grounding "Search Suggestions" HTML. JavaScript stays - * disabled (the content is static HTML/CSS); link taps are intercepted and opened - * in the system browser rather than navigating inside the WebView. + * Measure the rendered widget height so the WebView can size to its content. + * Google's snippet is trusted API output; only this script runs in the WebView. + */ +const MEASURE_HEIGHT_SCRIPT = ` +(function() { + function measure() { + var height = Math.max( + document.documentElement ? document.documentElement.scrollHeight : 0, + document.body ? document.body.scrollHeight : 0 + ); + if (height > 0 && window.ReactNativeWebView) { + window.ReactNativeWebView.postMessage(String(height)); + } + } + measure(); + window.addEventListener('load', measure); + setTimeout(measure, 100); +})(); +true; +` + +/** Wrap the API snippet in a minimal document shell without modifying the snippet itself. */ +function wrapGroundingHtmlSnippet(html: string): string { + return ` + + + + + + +${html} +` +} + +interface GroundingHtmlWebViewProps { + html: string + minHeight: number + containerStyle: ViewStyle +} + +function GroundingHtmlWebView({ html, minHeight, containerStyle }: GroundingHtmlWebViewProps) { + const [height, setHeight] = useState(minHeight) + const wrappedHtml = useMemo(() => wrapGroundingHtmlSnippet(html), [html]) + + return ( + + { + const measured = Number.parseInt(event.nativeEvent.data, 10) + if (Number.isFinite(measured) && measured > 0) { + setHeight(Math.min(Math.max(measured, minHeight), MAX_HEIGHT)) + } + }} + onShouldStartLoadWithRequest={(request) => { + if (request.url === 'about:blank') { + return true + } + if (isSafeHttpUrl(request.url)) { + void Linking.openURL(request.url).catch((error) => { + console.warn('Failed to open search suggestion URL', error) + }) + } + return false + }} + /> + + ) +} + +/** + * Native renderer for the grounding "Search Suggestions" HTML. Link taps are + * intercepted and opened in the system browser rather than navigating inside + * the WebView. + * + * WebViews inside inverted FlatLists are prone to painting over sibling rows on + * native. The clipped wrapper, fixed flex, opacity workaround, and + * removeClippedSubviews={false} on GiftedChat (see ChatView) keep the widget + * contained within its message bubble. * * The web build resolves to GroundingHtml.web.tsx (react-native-webview has no * web support) which renders the same HTML in a sandboxed
Suggestion
', + ) + + expect(headMarkup).toContain('.row') + expect(headMarkup).not.toContain('') + }) }) describe('formatGroundingShadowHtml', () => { @@ -110,4 +121,19 @@ describe('applyHorizontalWheelToScrollport', () => { expect(scrolled).toBe(true) expect(scrollEl.scrollLeft).toBe(10) }) + + it('returns false when the scrollport is already at the horizontal edge', () => { + const scrollEl = document.createElement('div') + Object.defineProperty(scrollEl, 'scrollWidth', { configurable: true, value: 400 }) + Object.defineProperty(scrollEl, 'clientWidth', { configurable: true, value: 200 }) + scrollEl.scrollLeft = 200 + + const scrolled = applyHorizontalWheelToScrollport( + { deltaX: 10, deltaY: 0, shiftKey: false }, + scrollEl, + ) + + expect(scrolled).toBe(false) + expect(scrollEl.scrollLeft).toBe(200) + }) }) diff --git a/src/utils/groundingShadowContent.ts b/src/utils/groundingShadowContent.ts index ca327a60..baaa556f 100644 --- a/src/utils/groundingShadowContent.ts +++ b/src/utils/groundingShadowContent.ts @@ -20,29 +20,115 @@ export const GROUNDING_SHADOW_HOST_CSS = ` } ` -/** Strip executable markup and non-http(s) hrefs before rendering. */ -export function sanitizeGroundingHtmlLinks(html: string): { headMarkup: string; bodyHtml: string } { - const doc = new DOMParser().parseFromString(html, 'text/html') - for (const script of Array.from(doc.querySelectorAll('script'))) { - script.remove() +/** Tags expected in Google's Search Suggestions widget HTML. */ +const GROUNDING_ALLOWED_BODY_TAGS = new Set([ + 'a', + 'b', + 'br', + 'div', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hr', + 'i', + 'img', + 'li', + 'ol', + 'p', + 'path', + 'polygon', + 'polyline', + 'rect', + 'span', + 'strong', + 'svg', + 'ul', +]) + +const GROUNDING_GLOBAL_ALLOWED_ATTRS = new Set([ + 'class', + 'dir', + 'id', + 'lang', + 'role', + 'style', + 'title', +]) + +const GROUNDING_TAG_ALLOWED_ATTRS: Record> = { + a: new Set(['href', 'rel', 'target']), + img: new Set(['alt', 'decoding', 'height', 'loading', 'src', 'width']), + path: new Set(['d', 'fill', 'stroke', 'stroke-width']), + svg: new Set(['fill', 'height', 'viewBox', 'width', 'xmlns']), +} + +const GROUNDING_URL_ATTRS = new Set(['href', 'src']) + +function isAllowedGroundingAttribute(tagName: string, attributeName: string): boolean { + const lowerName = attributeName.toLowerCase() + if (lowerName.startsWith('on') || lowerName.startsWith('data-') || lowerName.startsWith('aria-')) { + return lowerName.startsWith('aria-') || lowerName.startsWith('data-') + } + if (GROUNDING_GLOBAL_ALLOWED_ATTRS.has(lowerName)) { + return true + } + return GROUNDING_TAG_ALLOWED_ATTRS[tagName]?.has(lowerName) ?? false +} + +function sanitizeGroundingUrlAttribute(element: Element, attributeName: string): void { + const value = element.getAttribute(attributeName) + if (!value || !isSafeHttpUrl(value)) { + element.removeAttribute(attributeName) + return + } + if (element.tagName.toLowerCase() === 'a') { + element.setAttribute('target', '_blank') + element.setAttribute('rel', 'noopener noreferrer') + } +} + +function unwrapDisallowedElement(element: Element): void { + const parent = element.parentNode + if (!parent) { + element.remove() + return } - for (const element of Array.from(doc.querySelectorAll('*'))) { + while (element.firstChild) { + parent.insertBefore(element.firstChild, element) + } + parent.removeChild(element) +} + +function sanitizeGroundingElementTree(root: ParentNode): void { + for (const element of Array.from(root.querySelectorAll('*'))) { + const tagName = element.tagName.toLowerCase() + if (!GROUNDING_ALLOWED_BODY_TAGS.has(tagName)) { + unwrapDisallowedElement(element) + continue + } + for (const attribute of Array.from(element.attributes)) { - if (attribute.name.toLowerCase().startsWith('on')) { + const attributeName = attribute.name.toLowerCase() + if (!isAllowedGroundingAttribute(tagName, attributeName)) { element.removeAttribute(attribute.name) + continue + } + if (GROUNDING_URL_ATTRS.has(attributeName)) { + sanitizeGroundingUrlAttribute(element, attribute.name) } } } - for (const anchor of Array.from(doc.querySelectorAll('a[href]'))) { - const href = anchor.getAttribute('href') - if (!href || !isSafeHttpUrl(href)) { - anchor.removeAttribute('href') - } else { - anchor.setAttribute('target', '_blank') - anchor.setAttribute('rel', 'noopener noreferrer') - } - } - const headMarkup = Array.from(doc.head.querySelectorAll('style, link[rel="stylesheet"]')) +} + +/** Allowlist sanitizer for Google widget HTML before shadow-root injection. */ +export function sanitizeGroundingHtmlLinks(html: string): { headMarkup: string; bodyHtml: string } { + const doc = new DOMParser().parseFromString(html, 'text/html') + sanitizeGroundingElementTree(doc.body) + const headMarkup = Array.from(doc.head.querySelectorAll('style')) .map((el) => el.outerHTML) .join('') return { headMarkup, bodyHtml: doc.body.innerHTML } @@ -173,6 +259,11 @@ export function applyHorizontalWheelToScrollport( if (delta === 0 || scrollEl.scrollWidth <= scrollEl.clientWidth) { return false } - scrollEl.scrollLeft += delta + const maxScrollLeft = scrollEl.scrollWidth - scrollEl.clientWidth + const nextScrollLeft = Math.min(Math.max(scrollEl.scrollLeft + delta, 0), maxScrollLeft) + if (nextScrollLeft === scrollEl.scrollLeft) { + return false + } + scrollEl.scrollLeft = nextScrollLeft return true } From 2cc1bdc961825ae467eb9312dc46989ab366cac8 Mon Sep 17 00:00:00 2001 From: equationalapplications Date: Thu, 25 Jun 2026 18:29:46 -0400 Subject: [PATCH 06/11] fix(security): harden grounding HTML sanitization for CodeQL Use iterative replaceUntilStable for script and event-handler stripping, and allow whitespace before script closing tags. Co-authored-by: Cursor --- .../__tests__/sanitizeGroundingHtml.test.ts | 10 +++++++++ src/utils/sanitizeGroundingHtml.ts | 22 ++++++++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/utils/__tests__/sanitizeGroundingHtml.test.ts b/src/utils/__tests__/sanitizeGroundingHtml.test.ts index 3ef3a66a..0784fa98 100644 --- a/src/utils/__tests__/sanitizeGroundingHtml.test.ts +++ b/src/utils/__tests__/sanitizeGroundingHtml.test.ts @@ -14,6 +14,16 @@ describe('stripExecutableGroundingMarkup', () => { const html = '' expect(stripExecutableGroundingMarkup(html)).toBe('') }) + + it('removes script tags with whitespace before the closing angle bracket', () => { + const html = '
ok
more' + expect(stripExecutableGroundingMarkup(html)).toBe('
ok
more') + }) + + it('removes nested script tag obfuscation', () => { + const html = 'ipt>alert(1)ipt>' + expect(stripExecutableGroundingMarkup(html)).toBe('') + }) }) describe('sanitizeGroundingHtmlLinksRegex', () => { diff --git a/src/utils/sanitizeGroundingHtml.ts b/src/utils/sanitizeGroundingHtml.ts index 4a4630b7..9712475e 100644 --- a/src/utils/sanitizeGroundingHtml.ts +++ b/src/utils/sanitizeGroundingHtml.ts @@ -1,17 +1,29 @@ import { isSafeHttpUrl } from '~/utils/isSafeHttpUrl' -const SCRIPT_TAG_RE = /)<[^<]*)*<\/script>/gi +const SCRIPT_TAG_RE = /)<[^<]*)*<\/script\s*>/gi const SCRIPT_SELF_CLOSING_RE = /]*\/?>/gi const INLINE_EVENT_HANDLER_RE = /\s+on[a-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi const ANCHOR_TAG_RE = /]*)>/gi const HREF_ATTR_RE = /\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i +/** Repeat replace until stable — avoids CodeQL incomplete-sanitization bypasses. */ +function replaceUntilStable(html: string, pattern: RegExp): string { + let result = html + let previous = '' + while (result !== previous) { + previous = result + result = result.replace(new RegExp(pattern.source, pattern.flags), '') + } + return result +} + /** Remove script tags and inline event handlers from API-provided HTML snippets. */ export function stripExecutableGroundingMarkup(html: string): string { - return html - .replace(SCRIPT_TAG_RE, '') - .replace(SCRIPT_SELF_CLOSING_RE, '') - .replace(INLINE_EVENT_HANDLER_RE, '') + let result = html + result = replaceUntilStable(result, SCRIPT_TAG_RE) + result = replaceUntilStable(result, SCRIPT_SELF_CLOSING_RE) + result = replaceUntilStable(result, INLINE_EVENT_HANDLER_RE) + return result } /** Strip unsafe hrefs from anchor tags (native path; web uses DOMParser). */ From d0c0be439d4d3229eae31f0c7d7d47527b1d5979 Mon Sep 17 00:00:00 2001 From: equationalapplications Date: Thu, 25 Jun 2026 18:31:46 -0400 Subject: [PATCH 07/11] fix(chat): address Copilot review on grounding and generateReply Skip retry when empty responses have non-retryable finish reasons, remove script/style entirely in shadow sanitizer, fix svg viewBox allowlist casing, and limit removeClippedSubviews to native only. Co-authored-by: Cursor --- functions/src/generateReply.ts | 1 + src/components/ChatView.tsx | 7 +++---- src/utils/__tests__/groundingShadowContent.test.ts | 9 +++++++++ src/utils/groundingShadowContent.ts | 11 +++++++++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/functions/src/generateReply.ts b/functions/src/generateReply.ts index 73b8f9a3..d193f933 100644 --- a/functions/src/generateReply.ts +++ b/functions/src/generateReply.ts @@ -402,6 +402,7 @@ function getTextGenerator(): GenerateTextFn { candidateCount: candidates.length, }, ); + break; } throw new HttpsError("internal", "Model returned an empty response."); diff --git a/src/components/ChatView.tsx b/src/components/ChatView.tsx index eb7a83a8..54cd9b59 100644 --- a/src/components/ChatView.tsx +++ b/src/components/ChatView.tsx @@ -26,10 +26,9 @@ const webMessageTextWrapStyle = { overflowWrap: 'anywhere', } as TextStyle -/** WebViews in inverted lists can paint over sibling rows unless clipping is disabled. */ -const groundingListViewProps: Pick, 'removeClippedSubviews'> = { - removeClippedSubviews: false, -} +/** Native WebViews in inverted lists can paint over sibling rows unless clipping is disabled. */ +const groundingListViewProps: Pick, 'removeClippedSubviews'> | undefined = + Platform.OS === 'web' ? undefined : { removeClippedSubviews: false } interface ChatViewProps { characterId: string diff --git a/src/utils/__tests__/groundingShadowContent.test.ts b/src/utils/__tests__/groundingShadowContent.test.ts index 6162fae0..2ef8f9bf 100644 --- a/src/utils/__tests__/groundingShadowContent.test.ts +++ b/src/utils/__tests__/groundingShadowContent.test.ts @@ -19,10 +19,19 @@ describe('sanitizeGroundingHtmlLinks', () => { expect(headMarkup).toContain('.chip') expect(bodyHtml).not.toContain('bad') expect(bodyHtml).toContain('good') }) + it('preserves svg viewBox on allowed svg elements', () => { + const { bodyHtml } = sanitizeGroundingHtmlLinks( + '', + ) + + expect(bodyHtml).toContain('viewBox="0 0 24 24"') + }) + it('removes embed-capable elements and external stylesheet links', () => { const { headMarkup, bodyHtml } = sanitizeGroundingHtmlLinks( '
Suggestion
', diff --git a/src/utils/groundingShadowContent.ts b/src/utils/groundingShadowContent.ts index baaa556f..dfbfbfdc 100644 --- a/src/utils/groundingShadowContent.ts +++ b/src/utils/groundingShadowContent.ts @@ -63,9 +63,12 @@ const GROUNDING_TAG_ALLOWED_ATTRS: Record> = { a: new Set(['href', 'rel', 'target']), img: new Set(['alt', 'decoding', 'height', 'loading', 'src', 'width']), path: new Set(['d', 'fill', 'stroke', 'stroke-width']), - svg: new Set(['fill', 'height', 'viewBox', 'width', 'xmlns']), + svg: new Set(['fill', 'height', 'viewbox', 'width', 'xmlns']), } +/** Disallowed tags whose contents must not be preserved when removed. */ +const GROUNDING_REMOVE_ENTIRELY_TAGS = new Set(['script', 'style']) + const GROUNDING_URL_ATTRS = new Set(['href', 'src']) function isAllowedGroundingAttribute(tagName: string, attributeName: string): boolean { @@ -107,7 +110,11 @@ function sanitizeGroundingElementTree(root: ParentNode): void { for (const element of Array.from(root.querySelectorAll('*'))) { const tagName = element.tagName.toLowerCase() if (!GROUNDING_ALLOWED_BODY_TAGS.has(tagName)) { - unwrapDisallowedElement(element) + if (GROUNDING_REMOVE_ENTIRELY_TAGS.has(tagName)) { + element.remove() + } else { + unwrapDisallowedElement(element) + } continue } From d8713716449d3681c2b70dc5fe88bbe47a56a164 Mon Sep 17 00:00:00 2001 From: equationalapplications Date: Thu, 25 Jun 2026 18:34:33 -0400 Subject: [PATCH 08/11] fix(security): match malformed script closing tags in sanitizer Allow arbitrary non-> characters after are stripped per CodeQL bad-tag-filter. Co-authored-by: Cursor --- src/utils/__tests__/sanitizeGroundingHtml.test.ts | 5 +++++ src/utils/sanitizeGroundingHtml.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/utils/__tests__/sanitizeGroundingHtml.test.ts b/src/utils/__tests__/sanitizeGroundingHtml.test.ts index 0784fa98..ec5a7e70 100644 --- a/src/utils/__tests__/sanitizeGroundingHtml.test.ts +++ b/src/utils/__tests__/sanitizeGroundingHtml.test.ts @@ -20,6 +20,11 @@ describe('stripExecutableGroundingMarkup', () => { expect(stripExecutableGroundingMarkup(html)).toBe('
ok
more') }) + it('removes script tags with junk between the tag name and closing angle bracket', () => { + const html = '
ok
ipt>' expect(stripExecutableGroundingMarkup(html)).toBe('') diff --git a/src/utils/sanitizeGroundingHtml.ts b/src/utils/sanitizeGroundingHtml.ts index 9712475e..8a8e276c 100644 --- a/src/utils/sanitizeGroundingHtml.ts +++ b/src/utils/sanitizeGroundingHtml.ts @@ -1,6 +1,6 @@ import { isSafeHttpUrl } from '~/utils/isSafeHttpUrl' -const SCRIPT_TAG_RE = /)<[^<]*)*<\/script\s*>/gi +const SCRIPT_TAG_RE = /]*>)<[^<]*)*<\/script[^>]*>/gi const SCRIPT_SELF_CLOSING_RE = /]*\/?>/gi const INLINE_EVENT_HANDLER_RE = /\s+on[a-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi const ANCHOR_TAG_RE = /]*)>/gi From d1d5717b1d47c0460531722c885ef4152fc322f9 Mon Sep 17 00:00:00 2001 From: Kurt VanDusen Date: Thu, 25 Jun 2026 18:36:24 -0400 Subject: [PATCH 09/11] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Kurt VanDusen --- src/utils/groundingShadowContent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/groundingShadowContent.ts b/src/utils/groundingShadowContent.ts index dfbfbfdc..7e9bcbb4 100644 --- a/src/utils/groundingShadowContent.ts +++ b/src/utils/groundingShadowContent.ts @@ -152,7 +152,7 @@ export function mountGroundingShadowContent(host: HTMLElement, html: string): Sh const shadow = host.shadowRoot ?? host.attachShadow({ mode: 'open' }) shadow.innerHTML = formatGroundingShadowHtml(html) - if (!shadow.querySelector('[data-grounding-host-layout="true"]')) { + if (!shadow.querySelector('style[data-grounding-host-layout="true"]')) { const layoutStyle = document.createElement('style') layoutStyle.setAttribute('data-grounding-host-layout', 'true') layoutStyle.textContent = GROUNDING_SHADOW_HOST_CSS From eec13e74f3165dd2d3abac7198f569a0f8ce5cda Mon Sep 17 00:00:00 2001 From: Kurt VanDusen Date: Thu, 25 Jun 2026 18:36:38 -0400 Subject: [PATCH 10/11] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Kurt VanDusen --- src/components/GroundingHtml.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/GroundingHtml.web.tsx b/src/components/GroundingHtml.web.tsx index 4b026c0e..f5e08240 100644 --- a/src/components/GroundingHtml.web.tsx +++ b/src/components/GroundingHtml.web.tsx @@ -136,7 +136,7 @@ export function GroundingHtml({ html, style }: GroundingHtmlProps) { overflowY: 'hidden', boxSizing: 'border-box', WebkitOverflowScrolling: 'touch', - touchAction: 'pan-x', + touchAction: 'pan-x pan-y', ...containerStyle, }} onMouseDown={captureBubblePress} From bc8bc90d2810a370254155d95cbc174f1f49b8bd Mon Sep 17 00:00:00 2001 From: Kurt VanDusen Date: Thu, 25 Jun 2026 18:36:57 -0400 Subject: [PATCH 11/11] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Kurt VanDusen --- src/utils/sanitizeGroundingHtml.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/sanitizeGroundingHtml.ts b/src/utils/sanitizeGroundingHtml.ts index 8a8e276c..f5a41d79 100644 --- a/src/utils/sanitizeGroundingHtml.ts +++ b/src/utils/sanitizeGroundingHtml.ts @@ -23,6 +23,11 @@ export function stripExecutableGroundingMarkup(html: string): string { result = replaceUntilStable(result, SCRIPT_TAG_RE) result = replaceUntilStable(result, SCRIPT_SELF_CLOSING_RE) result = replaceUntilStable(result, INLINE_EVENT_HANDLER_RE) + result = replaceUntilStable( + result, + /<(?:iframe|object|embed|frame|frameset)\b[^>]*>(?:[\s\S]*?<\/(?:iframe|object|embed|frame|frameset)\s*>)?/gi, + ) + result = replaceUntilStable(result, /<(?:link|meta)\b[^>]*>/gi) return result }