Skip to content

fix: prevent timer leaks and add HTML sanitization layer#206

Open
msrofficial wants to merge 3 commits into
friuns2:mainfrom
msrofficial:main
Open

fix: prevent timer leaks and add HTML sanitization layer#206
msrofficial wants to merge 3 commits into
friuns2:mainfrom
msrofficial:main

Conversation

@msrofficial

@msrofficial msrofficial commented Jun 26, 2026

Copy link
Copy Markdown

🐛 Bugs Fixed

Medium: Timer Leaks

  • DirectoryHub.vue: Added onBeforeUnmount to clear toastTimer and composioSearchTimer when the component unmounts before timers fire
  • SkillsHub.vue: Added onBeforeUnmount to clear toastTimer on unmount

Low Risk: HTML Sanitization (Defense in Depth)

  • src/utils/sanitizeHtml.ts: Lightweight native DOMParser-based HTML sanitizer that strips <script>, <iframe>, <object>, event-handler attributes (onclick, onerror, etc.), and javascript: URLs. No external dependencies.
  • src/components/SafeHtml.vue: Reusable Vue component wrapping sanitizeHtml
  • ThreadConversation.vue: Added sanitizeHtml() to renderMarkdownBlocksAsHtml(), renderListItemContentAsHtml(), and renderCachedHighlightedCodeAsHtml() — all three v-html entry points
  • SkillDetailModal.vue: Added sanitizeHtml() to renderedReadme computed

✅ Verification

  • All v-for loops verified to have :key (42+ instances across codebase — all present)
  • No external dependencies added (uses native DOMParser API)
  • Zero unstaged changes, working tree clean

Summary by CodeRabbit

  • New Features

    • Added safer HTML rendering for content displayed in the app, including support for sanitized custom HTML blocks.
  • Bug Fixes

    • Improved protection against unsafe embedded content and malicious links in rendered text and conversation content.
    • Prevented delayed UI callbacks from running after certain panels and toasts are closed, reducing stray updates and errors.

… SkillsHub

- DirectoryHub.vue: Added onBeforeUnmount to clear toastTimer and composioSearchTimer on unmount
- SkillsHub.vue: Added onBeforeUnmount to clear toastTimer on unmount

Prevents timer leaks when components are unmounted before timers fire.
- Create src/utils/sanitizeHtml.ts: lightweight native DOMParser-based HTML sanitizer
  that strips script, iframe, object elements and event-handler attributes
- Create src/components/SafeHtml.vue: reusable wrapper component
- ThreadConversation.vue: add sanitizeHtml() to renderMarkdownBlocksAsHtml(),
  renderListItemContentAsHtml(), and renderCachedHighlightedCodeAsHtml()
- SkillDetailModal.vue: add sanitizeHtml() to renderedReadme computed
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a DOMParser-based HTML sanitizer and SafeHtml wrapper, routes several render paths through the sanitizer, and clears pending timers in two content components during unmount.

Changes

HTML sanitization path

Layer / File(s) Summary
Sanitizer utility and wrapper
src/utils/sanitizeHtml.ts, src/components/SafeHtml.vue
Adds sanitizeHtml and a SafeHtml component that renders sanitized HTML or its default slot.
README rendering sanitization
src/components/content/SkillDetailModal.vue
Wraps the generated README HTML in sanitizeHtml before it is returned for rendering.
Conversation render sanitization
src/components/content/ThreadConversation.vue
Sanitizes cached highlighted code HTML, list item HTML, and markdown block HTML before returning or caching the rendered strings.

Unmount timer cleanup

Layer / File(s) Summary
Timer cleanup on teardown
src/components/content/DirectoryHub.vue, src/components/content/SkillsHub.vue
Adds onBeforeUnmount hooks that clear pending toast and search timeouts before the components unmount.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

૮ᘏᗢ I munched the HTML, soft and neat,
With sanitizer crumbs and bunny feet.
Timers tucked in before I hop away,
Safe little renders brighten the day.
🥕✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: timer cleanup and adding an HTML sanitization layer.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix timer leaks and sanitize all v-html HTML rendering paths
🐞 Bug fix ✨ Enhancement 🕐 20-40 Minutes

Grey Divider

Description

• Clear pending toast/search timers on component unmount to prevent leaks.
• Add lightweight DOMParser-based HTML sanitizer (no new dependencies).
• Apply sanitization to all known v-html entry points and expose a SafeHtml wrapper.
Diagram

graph TD
  A["ThreadConversation.vue"] --> B["sanitizeHtml.ts"]
  C["SkillDetailModal.vue"] --> B["sanitizeHtml.ts"]
  D["SafeHtml.vue"] --> B["sanitizeHtml.ts"]
  E["DirectoryHub.vue"] --> F["onBeforeUnmount cleanup"]
  G["SkillsHub.vue"] --> F["onBeforeUnmount cleanup"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a vetted sanitizer library (e.g., DOMPurify)
  • ➕ More complete XSS coverage with community/audit history
  • ➕ Handles tricky edge cases (SVG/MathML, malformed markup, browser quirks)
  • ➖ Adds an external dependency (bundle size and supply-chain considerations)
  • ➖ May require configuration to match current allowed-tag intent
2. Eliminate v-html by rendering markdown to VNodes/components
  • ➕ Avoids HTML injection class of issues entirely
  • ➕ Better control over allowed elements and link handling
  • ➖ Larger refactor; higher complexity across markdown/list/code rendering
  • ➖ Potential performance and styling parity work

Recommendation: Given the stated “defense in depth” goal and desire to avoid new dependencies, the PR’s approach is reasonable as an incremental hardening step, especially since it targets the known v-html entry points. If the project expects untrusted/remote content over time, strongly consider switching to a vetted sanitizer (or removing v-html) to reduce exposure to edge-case XSS vectors beyond the current tag/attribute filtering.

Files changed (6) +115 / -5

Enhancement (2) +96 / -0
SafeHtml.vueAdd reusable SafeHtml wrapper component +25/-0

Add reusable SafeHtml wrapper component

• Introduces a small component that sanitizes a provided HTML string via a computed value and renders it through v-html. Falls back to slot content when no HTML is provided, and allows selecting the rendered tag via a prop.

src/components/SafeHtml.vue

sanitizeHtml.tsIntroduce lightweight DOMParser-based HTML sanitizer +71/-0

Introduce lightweight DOMParser-based HTML sanitizer

• Adds a native DOMParser sanitizer with allow/deny tag sets, stripping dangerous elements (e.g., script/iframe/object/style) and removing event-handler attributes. Also removes href/src/action values with javascript:/vbscript:/data: URL schemes, returning sanitized innerHTML.

src/utils/sanitizeHtml.ts

Bug fix (4) +19 / -5
DirectoryHub.vueClear toast and composio search timers on unmount +5/-1

Clear toast and composio search timers on unmount

• Adds onBeforeUnmount cleanup to cancel pending toast and composio search timeouts. Prevents timers from firing after the component has been destroyed.

src/components/content/DirectoryHub.vue

SkillDetailModal.vueSanitize rendered README HTML +2/-1

Sanitize rendered README HTML

• Routes the README markdown output through sanitizeHtml before it’s used as HTML. Reduces XSS risk from any markdown-derived HTML content.

src/components/content/SkillDetailModal.vue

SkillsHub.vueClear toast timer on unmount +4/-1

Clear toast timer on unmount

• Adds onBeforeUnmount cleanup to cancel any pending toast timeout. Avoids post-unmount timer execution and potential memory leaks.

src/components/content/SkillsHub.vue

ThreadConversation.vueSanitize all v-html HTML render paths in thread conversation +8/-2

Sanitize all v-html HTML render paths in thread conversation

• Adds sanitizeHtml to the markdown block renderer, list-item HTML renderer, and highlighted-code HTML output (including a wrapper around the cached highlighter). Ensures all primary HTML-string generation paths feeding v-html are sanitized consistently.

src/components/content/ThreadConversation.vue

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. sanitizeHtml missing manual tests 📘 Rule violation ⚙ Maintainability
Description
This PR changes user-visible behavior by sanitizing multiple v-html markdown/HTML rendering entry
points and updates lifecycle behavior by clearing toastTimer/composioSearchTimer on unmount to
prevent timer leaks, but it does not include corresponding updates to manual test documentation
under tests/<domain>/. Without updated verification steps and expectations, these changes are
harder to validate and navigation/unmount regressions may go unnoticed.
Code

src/components/content/ThreadConversation.vue[R3759-3766]

    markdownHtmlCache.set(cacheKey, cached)
    return cached.html
  }
-  const html = parseMessageBlocks(text)
+  const rawHtml = parseMessageBlocks(text)
    .map((block) => renderMessageBlockAsHtml(block))
    .join('')
+  const html = sanitizeHtml(rawHtml)
  return setBoundedCacheEntry(
Evidence
PR Compliance ID 1 requires updating domain manual test docs when behavior changes so the changes
remain verifiable and discoverable. In this PR, sanitization is introduced in
renderMarkdownBlocksAsHtml() (a v-html entry path), yet the chat composer/message rendering
manual test index has no new or updated section documenting how to verify the rendering/sanitization
behavior; similarly, the PR adds onBeforeUnmount() cleanup for timers in Skills/Directory hub
surfaces, but the Skills/Plugins/Integrations manual test index shows no added or updated section
describing how to validate unmount/navigation scenarios and confirm the timer-leak fix.

AGENTS.md: Update Manual Test Documentation for Feature Work (tests/<domain>/ and tests.md index)
src/components/content/ThreadConversation.vue[3754-3766]
src/utils/sanitizeHtml.ts[1-71]
tests/chat-composer-rendering/index.md[1-43]
src/components/content/DirectoryHub.vue[876-882]
src/components/content/SkillsHub.vue[175-178]
tests/skills-plugins-integrations/index.md[1-37]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR introduces behavior changes (sanitizing HTML/markdown rendering via `v-html` entry points and clearing timers on component unmount to prevent leaks), but it does not add or update the relevant manual test documentation under `tests/<domain>/`, making these changes harder to verify and more likely to regress unnoticed.

## Issue Context
PR Compliance ID 1 requires that behavior changes remain verifiable and discoverable via updates to domain manual test docs. The manual test docs should include prerequisites, exact actions/steps, expected results, and rollback/cleanup notes when applicable—especially for user-visible rendering changes and navigation/unmount scenarios.

## Fix Focus Areas
- tests/chat-composer-rendering/index.md[7-43]
- tests/skills-plugins-integrations/index.md[7-37]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. URL scheme bypass 🐞 Bug ⛨ Security
Description
sanitizeHtml() checks href/src/action with startsWith() on the raw attribute value without
trimming/normalizing, so values like " \njavascript:..." bypass the filter and remain in the output.
Because the sanitized result is injected via v-html (e.g., SkillDetailModal/ThreadConversation),
this can re-enable XSS via crafted links/images.
Code

src/utils/sanitizeHtml.ts[R51-66]

+    // Remove dangerous attributes (event handlers, javascript: URLs)
+    const attrs = el.attributes
+    for (let i = attrs.length - 1; i >= 0; i--) {
+      const attrName = attrs[i].name.toLowerCase()
+      const attrValue = attrs[i].value.toLowerCase()
+
+      if (attrName.startsWith('on')) {
+        el.removeAttribute(attrs[i].name)
+        continue
+      }
+
+      if (attrName === 'href' || attrName === 'src' || attrName === 'action') {
+        if (attrValue.startsWith('javascript:') || attrValue.startsWith('vbscript:') || attrValue.startsWith('data:')) {
+          el.removeAttribute(attrs[i].name)
+        }
+      }
Evidence
The sanitizer’s URL blocking logic lowercases but does not trim/normalize the attribute value, so a
dangerous scheme preceded by whitespace/control characters will not match the startsWith() guards.
The sanitized output is then inserted into the DOM via v-html in multiple components, making the
bypass directly exploitable if attacker-controlled HTML reaches these paths.

src/utils/sanitizeHtml.ts[51-66]
src/components/content/SkillDetailModal.vue[27-33]
src/components/content/ThreadConversation.vue[322-336]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`sanitizeHtml()` blocks `javascript:`, `vbscript:`, and `data:` URLs only when the attribute value *starts with* those strings. Because the current implementation does not trim/strip leading whitespace/control characters (and doesn’t otherwise normalize URLs), dangerous schemes can bypass the check.

## Issue Context
The sanitizer output is used in `v-html` sinks (e.g., `SkillDetailModal.vue` and `ThreadConversation.vue`), so bypasses directly affect DOM insertion.

## Fix Focus Areas
- src/utils/sanitizeHtml.ts[51-66]

## Suggested fix
- Normalize attribute values before checking schemes (at minimum: `const normalized = attrs[i].value.trim().toLowerCase()`), and consider stripping ASCII control chars (`\u0000`-`\u001F`, `\u007F`) before evaluation.
- Prefer a protocol-based check using `new URL(value, base)` where applicable and reject any non-allowlisted protocols (e.g., allow `http:`, `https:`, `mailto:`, `tel:` for `href`; allow `http:`/`https:` for `src`).
- If you keep string checks, ensure they handle leading whitespace and mixed whitespace like `"\t\njavascript:"`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Redundant sanitization overhead 🐞 Bug ➹ Performance
Description
ThreadConversation sanitizes per code block/list item and then sanitizes the entire rendered message
again, causing repeated DOMParser parsing and element-walking for the same content. This reduces the
practical benefit of the existing highlight/markdown caches and can add noticeable CPU/jank on long
threads with many blocks.
Code

src/components/content/ThreadConversation.vue[R3629-3636]

function renderCachedHighlightedCodeAsHtml(language: string, value: string): string {
+  return sanitizeHtml(_renderCachedHighlightedCodeAsHtml(language, value))
+}
+
+function _renderCachedHighlightedCodeAsHtml(language: string, value: string): string {
  const cacheKey = `${highlightCacheVersion.value}\u0000${normalizeCodeLanguage(language)}\u0000${language}\u0000${value}`
  const cached = highlightHtmlCache.get(cacheKey)
  if (cached !== undefined) {
Evidence
The code now sanitizes cached highlight HTML on every call and sanitizes list item HTML, while the
outer message renderer sanitizes the whole assembled HTML again before caching. The sanitizer itself
performs a DOM parse and a full element traversal, so repeated calls materially increase per-render
cost.

src/components/content/ThreadConversation.vue[3629-3647]
src/components/content/ThreadConversation.vue[3681-3685]
src/components/content/ThreadConversation.vue[3754-3776]
src/utils/sanitizeHtml.ts[27-31]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ThreadConversation.vue` currently calls `sanitizeHtml()` at multiple layers:
- per highlighted code block (`renderCachedHighlightedCodeAsHtml`),
- per list item (`renderListItemContentAsHtml`),
- and again for the full message (`renderMarkdownBlocksAsHtml`).

Because `sanitizeHtml()` does a full `DOMParser().parseFromString()` plus a `querySelectorAll('*')` walk, this multiplies work and undercuts the intended benefit of caching.

## Issue Context
The full message HTML is already sanitized and cached in `renderMarkdownBlocksAsHtml()`. Per-block sanitization inside the HTML-construction helpers is therefore redundant for the `v-html` sink that uses `renderMarkdownBlocksAsHtml()`.

## Fix Focus Areas
- src/components/content/ThreadConversation.vue[3629-3647]
- src/components/content/ThreadConversation.vue[3681-3685]
- src/components/content/ThreadConversation.vue[3754-3776]
- src/utils/sanitizeHtml.ts[27-31]

## Suggested fix
- Remove the inner `sanitizeHtml()` calls in `renderCachedHighlightedCodeAsHtml()` and `renderListItemContentAsHtml()` and rely on the single sanitization in `renderMarkdownBlocksAsHtml()`.
 - OR, if defense-in-depth is desired at the block level, sanitize *before caching* and store sanitized strings in the relevant caches so repeated renders don’t re-run DOM parsing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines 3759 to 3766
markdownHtmlCache.set(cacheKey, cached)
return cached.html
}
const html = parseMessageBlocks(text)
const rawHtml = parseMessageBlocks(text)
.map((block) => renderMessageBlockAsHtml(block))
.join('')
const html = sanitizeHtml(rawHtml)
return setBoundedCacheEntry(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. sanitizehtml missing manual tests 📘 Rule violation ⚙ Maintainability

This PR changes user-visible behavior by sanitizing multiple v-html markdown/HTML rendering entry
points and updates lifecycle behavior by clearing toastTimer/composioSearchTimer on unmount to
prevent timer leaks, but it does not include corresponding updates to manual test documentation
under tests/<domain>/. Without updated verification steps and expectations, these changes are
harder to validate and navigation/unmount regressions may go unnoticed.
Agent Prompt
## Issue description
The PR introduces behavior changes (sanitizing HTML/markdown rendering via `v-html` entry points and clearing timers on component unmount to prevent leaks), but it does not add or update the relevant manual test documentation under `tests/<domain>/`, making these changes harder to verify and more likely to regress unnoticed.

## Issue Context
PR Compliance ID 1 requires that behavior changes remain verifiable and discoverable via updates to domain manual test docs. The manual test docs should include prerequisites, exact actions/steps, expected results, and rollback/cleanup notes when applicable—especially for user-visible rendering changes and navigation/unmount scenarios.

## Fix Focus Areas
- tests/chat-composer-rendering/index.md[7-43]
- tests/skills-plugins-integrations/index.md[7-37]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/utils/sanitizeHtml.ts
Comment on lines +51 to +66
// Remove dangerous attributes (event handlers, javascript: URLs)
const attrs = el.attributes
for (let i = attrs.length - 1; i >= 0; i--) {
const attrName = attrs[i].name.toLowerCase()
const attrValue = attrs[i].value.toLowerCase()

if (attrName.startsWith('on')) {
el.removeAttribute(attrs[i].name)
continue
}

if (attrName === 'href' || attrName === 'src' || attrName === 'action') {
if (attrValue.startsWith('javascript:') || attrValue.startsWith('vbscript:') || attrValue.startsWith('data:')) {
el.removeAttribute(attrs[i].name)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Url scheme bypass 🐞 Bug ⛨ Security

sanitizeHtml() checks href/src/action with startsWith() on the raw attribute value without
trimming/normalizing, so values like " \njavascript:..." bypass the filter and remain in the output.
Because the sanitized result is injected via v-html (e.g., SkillDetailModal/ThreadConversation),
this can re-enable XSS via crafted links/images.
Agent Prompt
## Issue description
`sanitizeHtml()` blocks `javascript:`, `vbscript:`, and `data:` URLs only when the attribute value *starts with* those strings. Because the current implementation does not trim/strip leading whitespace/control characters (and doesn’t otherwise normalize URLs), dangerous schemes can bypass the check.

## Issue Context
The sanitizer output is used in `v-html` sinks (e.g., `SkillDetailModal.vue` and `ThreadConversation.vue`), so bypasses directly affect DOM insertion.

## Fix Focus Areas
- src/utils/sanitizeHtml.ts[51-66]

## Suggested fix
- Normalize attribute values before checking schemes (at minimum: `const normalized = attrs[i].value.trim().toLowerCase()`), and consider stripping ASCII control chars (`\u0000`-`\u001F`, `\u007F`) before evaluation.
- Prefer a protocol-based check using `new URL(value, base)` where applicable and reject any non-allowlisted protocols (e.g., allow `http:`, `https:`, `mailto:`, `tel:` for `href`; allow `http:`/`https:` for `src`).
- If you keep string checks, ensure they handle leading whitespace and mixed whitespace like `"\t\njavascript:"`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/components/content/ThreadConversation.vue (1)

3629-3633: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Sanitize once at the final render boundary.

renderMarkdownBlocksAsHtml() already sanitizes the full rawHtml before caching, so sanitizing each code/list fragment adds repeated blocking DOMParser work for large messages.

Proposed refactor
 function renderCachedHighlightedCodeAsHtml(language: string, value: string): string {
-  return sanitizeHtml(_renderCachedHighlightedCodeAsHtml(language, value))
-}
-
-function _renderCachedHighlightedCodeAsHtml(language: string, value: string): string {
   const cacheKey = `${highlightCacheVersion.value}\u0000${normalizeCodeLanguage(language)}\u0000${language}\u0000${value}`
   const cached = highlightHtmlCache.get(cacheKey)
   if (cached !== undefined) {
@@
 function renderListItemContentAsHtml(item: ListItem): string {
   const paragraphsHtml = renderListItemParagraphsAsHtml(item)
   const childrenHtml = item.children?.map((block) => renderMessageBlockAsHtml(block)).join('') ?? ''
-  return sanitizeHtml(paragraphsHtml + childrenHtml)
+  return paragraphsHtml + childrenHtml
 }

Also applies to: 3681-3684, 3762-3765

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/content/ThreadConversation.vue` around lines 3629 - 3633, The
highlighted code/list fragment helpers are sanitizing too early and duplicating
expensive DOMParser work because renderMarkdownBlocksAsHtml already sanitizes
the full HTML before caching. Update renderCachedHighlightedCodeAsHtml,
renderCachedMarkdownListAsHtml, and the related fragment render helper paths so
they return raw rendered HTML and rely on the final render boundary to sanitize
once, keeping the existing caching behavior intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/content/DirectoryHub.vue`:
- Around line 879-882: The timer cleanup in DirectoryHub.vue is incomplete
because async work can still call showToast() or mutate state after unmount. Add
a component-level disposed flag such as isUnmounted, set it in
onBeforeUnmount(), and have showToast plus any post-await state updates in this
component check that flag before creating new timeouts or writing state.
Reference the existing showToast function and the onBeforeUnmount teardown so
the guard covers all late-firing async paths.

In `@src/components/content/SkillsHub.vue`:
- Around line 176-178: The toast cleanup in SkillsHub is incomplete because
onBeforeUnmount only clears the current timer, but async handlers can still call
showToast() after teardown and recreate it. Add an isUnmounted guard in
showToast and any post-await flows in the same component so no new timeout or
state update is scheduled once unmounted. Use the existing showToast and
onBeforeUnmount logic in SkillsHub.vue as the places to wire this guard and
prevent late resolves from re-arming the timer.

In `@src/utils/sanitizeHtml.ts`:
- Around line 23-24: The fallback in sanitizeHtml currently returns the original
HTML when DOMParser is unavailable, so make it fail closed by escaping or
neutralizing the input instead of passing it through unchanged. In sanitizeHtml
and the URL attribute checks around the scheme validation logic, normalize
candidate URLs by stripping whitespace and control characters before testing for
dangerous schemes, so payloads like href with embedded newlines or tabs are
caught consistently. Use the existing sanitizeHtml flow and its attribute
filtering helpers to apply the fix in one place, including the href/src scheme
handling path.

---

Nitpick comments:
In `@src/components/content/ThreadConversation.vue`:
- Around line 3629-3633: The highlighted code/list fragment helpers are
sanitizing too early and duplicating expensive DOMParser work because
renderMarkdownBlocksAsHtml already sanitizes the full HTML before caching.
Update renderCachedHighlightedCodeAsHtml, renderCachedMarkdownListAsHtml, and
the related fragment render helper paths so they return raw rendered HTML and
rely on the final render boundary to sanitize once, keeping the existing caching
behavior intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d6676400-339e-4716-8d29-f0d48a2b42e2

📥 Commits

Reviewing files that changed from the base of the PR and between fac2291 and f505a64.

📒 Files selected for processing (6)
  • src/components/SafeHtml.vue
  • src/components/content/DirectoryHub.vue
  • src/components/content/SkillDetailModal.vue
  • src/components/content/SkillsHub.vue
  • src/components/content/ThreadConversation.vue
  • src/utils/sanitizeHtml.ts

Comment on lines +879 to +882
onBeforeUnmount(() => {
if (toastTimer) clearTimeout(toastTimer)
if (composioSearchTimer) clearTimeout(composioSearchTimer)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Cleanup is incomplete without a disposed guard.

This clears timers that already exist, but async actions in this component can still finish after unmount and call showToast(), which creates a brand-new timeout on a destroyed instance. Guard showToast/post-await state writes with an isUnmounted flag so teardown cannot recreate the leak you’re trying to fix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/content/DirectoryHub.vue` around lines 879 - 882, The timer
cleanup in DirectoryHub.vue is incomplete because async work can still call
showToast() or mutate state after unmount. Add a component-level disposed flag
such as isUnmounted, set it in onBeforeUnmount(), and have showToast plus any
post-await state updates in this component check that flag before creating new
timeouts or writing state. Reference the existing showToast function and the
onBeforeUnmount teardown so the guard covers all late-firing async paths.

Comment on lines +176 to +178
onBeforeUnmount(() => {
if (toastTimer) clearTimeout(toastTimer)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

The timer can still be recreated after unmount.

onBeforeUnmount only cancels the current handle. If an in-flight install/search/toggle request resolves later, its showToast() call will schedule a new timeout after teardown. Please pair this with an isUnmounted guard in showToast (and any other post-await state updates) to actually close the leak.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/content/SkillsHub.vue` around lines 176 - 178, The toast
cleanup in SkillsHub is incomplete because onBeforeUnmount only clears the
current timer, but async handlers can still call showToast() after teardown and
recreate it. Add an isUnmounted guard in showToast and any post-await flows in
the same component so no new timeout or state update is scheduled once
unmounted. Use the existing showToast and onBeforeUnmount logic in SkillsHub.vue
as the places to wire this guard and prevent late resolves from re-arming the
timer.

Comment thread src/utils/sanitizeHtml.ts
Comment on lines +23 to +24
export function sanitizeHtml(raw: string): string {
if (typeof DOMParser === 'undefined') return raw

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Make the sanitizer fail closed and normalize URL schemes.

Line 24 returns unsanitized HTML when DOMParser is unavailable, and Lines 55-64 miss payloads such as href="\njavascript:...". Escape on fallback and strip whitespace/control characters before checking dangerous schemes.

Proposed fix
 const DENIED_TAGS = new Set([
   'script', 'style', 'iframe', 'frame', 'object', 'embed', 'param',
   'applet', 'link', 'base', 'meta', 'noscript',
 ])
 
+const URL_ATTRIBUTES = new Set(['href', 'src', 'action', 'formaction', 'xlink:href'])
+
+function escapeHtml(raw: string): string {
+  return raw
+    .replace(/&/gu, '&amp;')
+    .replace(/</gu, '&lt;')
+    .replace(/>/gu, '&gt;')
+    .replace(/"/gu, '&quot;')
+    .replace(/'/gu, '&`#39`;')
+}
+
+function isUnsafeUrl(value: string): boolean {
+  const normalized = value.replace(/[\u0000-\u001F\u007F\s]+/gu, '').toLowerCase()
+  return /^(?:javascript|vbscript|data):/u.test(normalized)
+}
+
 /**
  * Sanitize an HTML string in the browser.
  * Parses the HTML, strips forbidden tags and attributes, and returns clean HTML.
  */
 export function sanitizeHtml(raw: string): string {
-  if (typeof DOMParser === 'undefined') return raw
+  if (typeof DOMParser === 'undefined') return escapeHtml(raw)
   if (!raw) return ''
@@
-      const attrValue = attrs[i].value.toLowerCase()
-
       if (attrName.startsWith('on')) {
         el.removeAttribute(attrs[i].name)
         continue
       }
 
-      if (attrName === 'href' || attrName === 'src' || attrName === 'action') {
-        if (attrValue.startsWith('javascript:') || attrValue.startsWith('vbscript:') || attrValue.startsWith('data:')) {
+      if (URL_ATTRIBUTES.has(attrName)) {
+        if (isUnsafeUrl(attrs[i].value)) {
           el.removeAttribute(attrs[i].name)
         }
       }

Also applies to: 51-65

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/sanitizeHtml.ts` around lines 23 - 24, The fallback in sanitizeHtml
currently returns the original HTML when DOMParser is unavailable, so make it
fail closed by escaping or neutralizing the input instead of passing it through
unchanged. In sanitizeHtml and the URL attribute checks around the scheme
validation logic, normalize candidate URLs by stripping whitespace and control
characters before testing for dangerous schemes, so payloads like href with
embedded newlines or tabs are caught consistently. Use the existing sanitizeHtml
flow and its attribute filtering helpers to apply the fix in one place,
including the href/src scheme handling path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant