Skip to content

Security fixes#219

Merged
phillipc merged 11 commits into
knockout:mainfrom
Auge19:security_fixes
Dec 20, 2025
Merged

Security fixes#219
phillipc merged 11 commits into
knockout:mainfrom
Auge19:security_fixes

Conversation

@phillipc
Copy link
Copy Markdown
Member

@phillipc phillipc commented Dec 14, 2025

  • feature) new HTML sanitization option
  • fix) some codeQL-Warnings

Summary by CodeRabbit

  • New Features

    • Configurable HTML template sanitization hook with a one-time warning when no sanitizer is configured
  • Bug Fixes

    • Prevented prototype pollution in DOM data handling
    • Improved HTML input validation and safer template parsing
  • Documentation

    • Added "Sanitizing HTML-Templates" guide with examples and CSP recommendations
  • Refactor

    • Simplified internal control flows for reliability
  • Tests

    • Expanded tests and lifecycle hooks; added coverage for HTML-setting behavior

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 20, 2025

Walkthrough

Adds an HTML sanitization hook and integrates it into HTML parsing, prevents prototype-pollution in DOM data helpers, adjusts computed disposal option sourcing, refines mapping helpers for undefined roots, updates JSON parsing, and introduces test lifecycle helpers and test-time sanitizer behavior.

Changes

Cohort / File(s) Summary
Documentation
README.md
Adds "Sanitizing HTML-Templates" section describing options.sanitizeHtmlTemplate with DOMPurify/validator.js examples and recommends CSP.
Test Infrastructure
builds/knockout/helpers/jasmine.extensions.js
Adds KARMA_STRING, disableJQueryUsage, switchJQueryState() and global beforeEach/afterEach hooks to manage and verify jQuery state in tests.
Test Configuration
packages/binding.foreach/spec/eachBehavior.ts
Sets options.sanitizeHtmlTemplate to an identity (no-op) sanitizer for tests.
HTML Parsing & Sanitization
packages/utils/src/dom/html.ts
Adds validateHTMLInput() returning sanitized string; parseHtmlFragment() and setHtml() use sanitized HTML; parsing strategy selection refactored (prefer template, optional jQuery, then simple).
Sanitization Hook
packages/utils/src/options.ts
Adds private _sanitizeWarningLogged and public sanitizeHtmlTemplate(html: string): string which logs a one-time warning and returns input when no sanitizer configured.
Security & DOM Data
packages/utils/src/dom/data.ts
Prevents prototype-pollution by rejecting unsafe keys (__proto__, constructor, prototype); initializes data store as empty object; adds isSafeKey() and guards in get/set/getOrSet.
Observable Utilities
packages/observable/src/mappingHelpers.ts
Changes mapJsObjectGraph signature to accept `rootObject: T
Core Logic Changes
packages/computed/src/computed.ts
Simplifies disposal initialization: disposeWhenNodeIsRemoved sourced only from options.disposeWhenNodeIsRemoved and disposeWhen only from options.disposeWhen (removes cross-option fallback).
Code Simplification
packages/bind/src/arrayToDomNodeChildren.ts
Removes redundant isFirstExecution check when assigning lastArray, simplifying branch logic without behavior change.
JSON Parsing Cleanup
packages/utils/src/string.ts
parseJson() now unconditionally uses JSON.parse() for non-empty trimmed input; removes legacy fallback branches.
Tests β€” parseHtmlFragment / setHtml
packages/utils/spec/parseHtmlFragmentBehavior.ts
Tests now import and exercise setHtml(node, html) alongside parseHtmlFragment, including error-path checks for script-tag inputs; relies on setHtml being exported from dist.
Build / Manifest updates
manifest_file, package.json
Minor manifest/package updates referenced in summary (no detail changes described).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–30 minutes

  • Areas needing extra attention:
    • packages/utils/src/dom/html.ts β€” ensure sanitized string flows to all code paths (parseHtmlFragment, setHtml, jQuery path).
    • packages/utils/src/dom/data.ts β€” verify safe-key checks don't block legitimate keys and cover all data access patterns.
    • packages/computed/src/computed.ts β€” confirm removal of cross-option fallback doesn't change computed disposal behavior unexpectedly.
    • packages/observable/src/mappingHelpers.ts β€” ensure handling of undefined rootObject doesn't introduce runtime errors where callers expect non-undefined.
    • packages/utils/spec/parseHtmlFragmentBehavior.ts β€” confirm setHtml export exists and tests reflect intended public API.

Poem

🐰 I nibble at HTML, tidy and neat,
I guard data keys from a sneaky deceit.
Tests flip jQuery like leaves in the breeze,
Parsers now whisper: "sanitize please."
Hop, scrub, and tidy β€” a safer code treat! πŸ₯•

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Security fixes' is vague and generic. While the PR does address security (HTML sanitization and CodeQL warnings), the title uses a non-descriptive term that doesn't convey the specific nature or scope of the changes. Consider a more specific title such as 'Add HTML sanitization and fix security vulnerabilities' or 'Implement HTML template sanitization and resolve CodeQL warnings'.
βœ… Passed checks (1 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • πŸ“ Generate docstrings
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/utils/src/dom/html.ts (1)

170-179: Critical: jQuery path bypasses HTML sanitization.

The return value from validateHTMLInput(html) at line 170 is discarded. When the jQuery path is taken (line 179), the original unsanitized html is passed to jQuery(node).html(html), completely bypassing the options.sanitizeHtmlTemplate() sanitization.

This defeats the purpose of the new sanitization feature for browsers where supportsTemplateTag is false and jQuery is present.

πŸ”Ž Proposed fix
     validateHTMLInput(html)
+    html = validateHTMLInput(html)
     const jQuery = options.jQuery
     // If the browser supports <template> tags, prefer that, as
     // it obviates all the complex workarounds of jQuery.

Alternatively, since parseHtmlFragment already sanitizes internally, you could simplify by always using it:

-    validateHTMLInput(html)
-    const jQuery = options.jQuery
-    // If the browser supports <template> tags, prefer that, as
-    // it obviates all the complex workarounds of jQuery.
-    //
-    // However, jQuery contains a lot of sophisticated code to parse arbitrary HTML fragments,
-    // for example <tr> elements which are not normally allowed to exist on their own.
-    // If you've referenced jQuery (and template tags are not supported) we'll use that rather than duplicating its code.
-    if (jQuery && !supportsTemplateTag) {
-      jQuery(node).html(html)
-    } else {
-            // ... otherwise, use KO's own parsing logic.
-      let parsedNodes : Node[]
-      if(node.ownerDocument) {
-        parsedNodes = parseHtmlFragment(html, node.ownerDocument)
-      }
-      else {
-        parsedNodes = parseHtmlFragment(html)
-      }
+    // Use KO's parsing logic which includes sanitization
+    let parsedNodes : Node[]
+    if(node.ownerDocument) {
+      parsedNodes = parseHtmlFragment(html, node.ownerDocument)
+    }
+    else {
+      parsedNodes = parseHtmlFragment(html)
+    }
 
-      if (node.nodeType === 8) {
-        if (html === null) {
-          virtualElements.emptyNode(node)
-        } else {
-          virtualElements.setDomNodeChildren(node, parsedNodes)
-        }
+    if (node.nodeType === 8) {
+      if (html === null) {
+        virtualElements.emptyNode(node)
       } else {
-        for (let i = 0; i < parsedNodes.length; i++) { node.appendChild(parsedNodes[i]) }
+        virtualElements.setDomNodeChildren(node, parsedNodes)
       }
+    } else {
+      for (let i = 0; i < parsedNodes.length; i++) { node.appendChild(parsedNodes[i]) }
     }
🧹 Nitpick comments (2)
packages/utils/src/options.ts (1)

95-104: Consider rate-limiting the warning message.

The sanitization hook is well-designed with clear documentation. However, the warning will be logged every time HTML is sanitized, which could spam the console during normal application use.

πŸ’‘ Suggested improvement to log warning only once
+ private _sanitizeWarningLogged: boolean = false
+
  /** 
   * Sanitize HTML templates before parsing them. Default is a no-op. 
   * Please configure something like DOMPurify or validator.js for your environment.
   * @param html HTML string to be sanitized
   * @returns Sanitized HTML string
   */
  sanitizeHtmlTemplate(html: string): string {
-    console.log('WARNING -- You don\'t have a HTML sanitizer configured. Please configure options.sanitizeHtmlTemplate to avoid XSS vulnerabilities.')
+    if (!this._sanitizeWarningLogged) {
+      console.warn('WARNING -- You don\'t have a HTML sanitizer configured. Please configure options.sanitizeHtmlTemplate to avoid XSS vulnerabilities.')
+      this._sanitizeWarningLogged = true
+    }
    return html
  }

Also consider using console.warn instead of console.log for warnings.

builds/knockout/helpers/jasmine.extensions.js (1)

206-214: Consider using const/let instead of var.

The logic correctly toggles jQuery usage based on Karma arguments. Minor suggestion: use modern variable declarations.

πŸ’‘ Optional modernization
 const KARMA_STRING = '__karma__'
-var disableJQueryUsage = true;
+let disableJQueryUsage = true;
 function switchJQueryState() {
πŸ“œ Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between f595f33 and 38ff52a.

πŸ“’ Files selected for processing (11)
  • README.md (1 hunks)
  • builds/knockout/helpers/jasmine.extensions.js (1 hunks)
  • packages/bind/src/arrayToDomNodeChildren.ts (1 hunks)
  • packages/binding.foreach/spec/eachBehavior.ts (1 hunks)
  • packages/computed/src/computed.ts (1 hunks)
  • packages/observable/src/mappingHelpers.ts (2 hunks)
  • packages/utils/helpers/jasmine-13-helper.ts (1 hunks)
  • packages/utils/src/dom/data.ts (3 hunks)
  • packages/utils/src/dom/html.ts (1 hunks)
  • packages/utils/src/options.ts (2 hunks)
  • packages/utils/src/string.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
packages/binding.foreach/spec/eachBehavior.ts (1)
packages/binding.core/src/options.ts (1)
  • options (13-172)
packages/bind/src/arrayToDomNodeChildren.ts (1)
packages/utils/src/array.ts (1)
  • arrayMap (25-28)
builds/knockout/helpers/jasmine.extensions.js (1)
tko.io/src/tko-io.js (1)
  • window (1-1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: testheadless (22.x, off)
  • GitHub Check: testheadless (22.x, on)
πŸ”‡ Additional comments (17)
packages/bind/src/arrayToDomNodeChildren.ts (1)

140-140: LGTM! Redundant condition removed.

The conditional check for isFirstExecution was redundant since this code is already within the else block where !isFirstExecution is true. The refactored version is cleaner and equivalent.

README.md (1)

109-114: LGTM! Clear security documentation added.

The documentation clearly explains the sanitization option and provides practical examples using well-known libraries. The recommendation to implement CSP adds an important defense-in-depth layer.

packages/utils/src/string.ts (1)

21-22: LGTM! Simplified and more secure.

Removing the Function-based fallback eliminates a potential code injection vector. All modern browsers (including IE8+) support JSON.parse natively, so this simplification is safe and improves security.

packages/utils/src/dom/data.ts (5)

9-9: LGTM! Safer initialization.

Initializing dataStore as an empty object instead of potentially undefined adds clarity and safety.


12-15: LGTM! Critical prototype pollution prevention.

The isSafeKey function effectively prevents prototype pollution attacks by blocking dangerous property names (__proto__, constructor, prototype). This is a crucial security fix.


78-83: LGTM! Proper validation in get method.

The unsafe key check with clear error message provides good protection and debugging information.


85-92: LGTM! Proper validation in set method.

Consistent validation pattern applied to the set method.


94-98: LGTM! Proper validation in getOrSet method.

All three data access methods now have consistent protection against prototype pollution.

packages/utils/helpers/jasmine-13-helper.ts (1)

250-253: LGTM! Appropriate test setup.

Disabling sanitization in tests with an identity function is appropriate to avoid log pollution during test runs while still exercising the code paths.

packages/binding.foreach/spec/eachBehavior.ts (1)

49-53: LGTM! Consistent test setup.

The test setup correctly disables sanitization warnings with an identity function, consistent with other test files in the PR.

builds/knockout/helpers/jasmine.extensions.js (2)

193-200: LGTM! Proper test lifecycle management.

The beforeEach hook appropriately sets up jQuery state and disables sanitization warnings for tests.


202-204: LGTM! Good state verification.

The afterEach assertion helps catch unintended modifications to the jQuery usage flag during tests.

packages/utils/src/dom/html.ts (2)

109-125: LGTM! Sanitization is correctly integrated into all parsing paths.

The centralized call to validateHTMLInput ensures all three parsing strategies (template, jQuery, simple) receive sanitized HTML consistently.


128-141: LGTM! Validation function correctly returns sanitized HTML.

The function now properly returns the sanitized HTML after performing size and script-tag checks. The empty string return for falsy input is a safe default.

packages/observable/src/mappingHelpers.ts (2)

29-29: LGTM: Signature change correctly handles undefined callback results.

The addition of | undefined to the parameter type allows mapInputCallback to return undefined (e.g., when unwrapping observables with undefined values) without type errors. The existing guard at lines 31-34 already handles undefined correctly by returning early when rootObject cannot have properties.


40-40: Non-null assertion is safe but relies on control flow guards.

The non-null assertion is correct because the guard at lines 31-34 ensures rootObject is not undefined before reaching line 40. However, this relies on understanding the control flow between the early return and the property access.

The change appears to address CodeQL warnings about potential undefined access while maintaining type safety through the existing guards.

packages/computed/src/computed.ts (1)

125-126: LGTM! Removal of cross-option fallback improves predictability and fixes CodeQL warnings.

The changes correctly remove the cross-option defaulting behavior between disposeWhenNodeIsRemoved and disposeWhen, making each disposal condition independent and explicit. Previously, setting disposeWhenNodeIsRemoved would override the disposeWhen function to combine both conditions. Now, each option is assigned independently and the disposal logic evaluates them with OR semantics (line 343), eliminating confusing control flow.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/utils/src/options.ts (1)

95-108: Consider using console.warn instead of console.log for the security warning.

The warning about missing HTML sanitizer configuration is a security concern and should be logged at the appropriate level using console.warn rather than console.log.

πŸ”Ž Proposed fix
   sanitizeHtmlTemplate(html: string): string {
     if (!this._sanitizeWarningLogged) {
-      console.log('WARNING -- You don\'t have a HTML sanitizer configured. Please configure options.sanitizeHtmlTemplate to avoid XSS vulnerabilities.')
+      console.warn('WARNING -- You don\'t have a HTML sanitizer configured. Please configure options.sanitizeHtmlTemplate to avoid XSS vulnerabilities.')
       this._sanitizeWarningLogged = true
     }
     return html
   }
packages/utils/src/dom/html.ts (1)

170-184: Consider the double sanitization in the parseHtmlFragment path.

Line 170 validates HTML for the jQuery.html() path (line 179), but when parseHtmlFragment is called at line 184, it validates again (line 110 in parseHtmlFragment). This double sanitization is safe but potentially inefficient if the sanitizer is expensive.

Consider refactoring to avoid redundant sanitization while maintaining security, such as:

  • Creating an internal _parseHtmlFragmentUnsafe that skips validation for use by setHtml
  • Or documenting this as intentional defense-in-depth
πŸ“œ Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 38ff52a and 84c21cd.

πŸ“’ Files selected for processing (3)
  • builds/knockout/helpers/jasmine.extensions.js (1 hunks)
  • packages/utils/src/dom/html.ts (2 hunks)
  • packages/utils/src/options.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • builds/knockout/helpers/jasmine.extensions.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: testheadless (22.x, off)
  • GitHub Check: testheadless (22.x, on)
πŸ”‡ Additional comments (2)
packages/utils/src/dom/html.ts (2)

109-125: Excellent security improvement with sanitization integration.

The restructured parsing logic correctly sanitizes HTML first via validateHTMLInput, then uses the sanitized result across all parsing strategies. The conditional order (template tag β†’ jQuery β†’ simple parse) is appropriate and well-structured.


128-141: Security improvement with sanitization return path.

The function now returns sanitized HTML after validation checks, enabling downstream code to use the safer result. Returning an empty string for falsy input is a safe default that prevents null/undefined edge cases.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/utils/src/options.ts (1)

95-108: LGTM! Well-designed sanitization hook with appropriate defaults.

The implementation correctly provides a customizable sanitization hook with a one-time warning to alert developers. The no-op default preserves backward compatibility while encouraging security best practices.

Optional: Consider enhancing the warning message

The current warning is clear, but you might consider making it more actionable by mentioning specific libraries:

-      console.warn('WARNING -- You don\'t have a HTML sanitizer configured. Please configure options.sanitizeHtmlTemplate to avoid XSS vulnerabilities.')
+      console.warn('WARNING -- You don\'t have an HTML sanitizer configured. Please configure options.sanitizeHtmlTemplate with a library like DOMPurify to avoid XSS vulnerabilities.')
packages/utils/spec/parseHtmlFragmentBehavior.ts (1)

134-147: LGTM! Good test coverage for setHtml sanitization.

The test correctly verifies that setHtml rejects HTML containing script tags, mirroring the existing parseHtmlFragment test pattern. This ensures both parsing paths properly enforce the security policy.

Optional: Consider removing debug console.log

Line 140 logs the return value of setHtml, which is always undefined. This appears to be debug code that could be removed for cleaner test output:

      try {    
        let ret = setHtml(node, html)
-        console.log(ret)
      } catch (e : any) {

Note: The same pattern exists on line 125 for parseHtmlFragment, so you may want to clean up both if desired.

πŸ“œ Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 84c21cd and dfdd586.

πŸ“’ Files selected for processing (3)
  • packages/utils/spec/parseHtmlFragmentBehavior.ts (2 hunks)
  • packages/utils/src/dom/html.ts (3 hunks)
  • packages/utils/src/options.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/utils/src/dom/html.ts (1)
packages/utils/src/options.ts (2)
  • jQuery (53-57)
  • jQuery (64-75)
packages/utils/spec/parseHtmlFragmentBehavior.ts (1)
packages/utils/src/dom/html.ts (1)
  • setHtml (156-202)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: testheadless (22.x, on)
  • GitHub Check: testheadless (22.x, off)
πŸ”‡ Additional comments (4)
packages/utils/src/dom/html.ts (3)

109-125: LGTM! Sanitization correctly applied across all parsing paths.

The refactored logic ensures that validateHTMLInput sanitizes the HTML before any parsing strategy is used. The preference order (template tag β†’ jQuery β†’ simple parser) is sensible and all paths use the sanitized saferHtml.


128-141: LGTM! Correct validation and sanitization order.

The signature change from void to string is appropriate, allowing the sanitized HTML to be used inline. The validation sequence is correct:

  1. Reject falsy input early
  2. Check size limits on original input
  3. Detect script tags on original input
  4. Sanitize and return

This ordering ensures that malicious content is detected before sanitization potentially masks it.


156-201: LGTM! Consistent sanitization across both setHtml code paths.

The function correctly sanitizes HTML in both execution paths:

  • jQuery path (line 179-180): Explicitly validates via validateHTMLInput before passing to jQuery(node).html()
  • parseHtmlFragment path (lines 184-189): Validation happens internally within parseHtmlFragment

This ensures HTML is sanitized regardless of which rendering strategy is used.

packages/utils/spec/parseHtmlFragmentBehavior.ts (1)

2-2: LGTM! Correctly imports setHtml for testing.

The addition of setHtml to the imports enables testing of the sanitization behavior in the jQuery code path.

@phillipc phillipc merged commit 9d86f13 into knockout:main Dec 20, 2025
5 checks passed
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