Skip to content

Potential XSS when HTML rendering is enabled without a reliable sanitizer #495

@WiiiiillYeng

Description

@WiiiiillYeng

Describe the bug

When renderOptionLabelAsHtml or useSelectOptionLabelToHtml is enabled, multiple-select-vanilla renders option-controlled content with innerHTML.

If no sanitizer option is provided, the value is inserted directly into innerHTML. When useSelectOptionLabelToHtml is enabled, the selected value displayed in the main choice button is passed through stripScripts() first, but that helper is based on a blacklist regex /(\b)(on[a-z]+)(\s*)=([^>]*)|javascript:([^>]*)[^>]*|(<\s*)(\/*)script([<>]*).*(<\s*)(\/*)script(>*)|(&lt;|&#60;)(\/*)(script|script defer)(.*)(&#62;|&gt;|&gt;">)/gi and does not provide complete HTML sanitization.

This can lead to XSS when an application enables HTML rendering and the option text or value can contain untrusted content.

Relevant code paths:

  • MultipleSelectInstance.isRenderAsHtml returns true when renderOptionLabelAsHtml or useSelectOptionLabelToHtml is true.
  • applyAsTextOrHtmlWhenEnabled(...) writes to innerHTML when HTML rendering is enabled.
  • useSelectOptionLabelToHtml uses selected option values in the main choice button and calls stripScripts(labels) before writing the result to innerHTML.
  • stripScripts(...) uses a regex blacklist for sanitizing.

Reproduction

Minimal HTML reproduction:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>multiple-select-vanilla XSS reproduction</title>
  <script type="module">
    import { multipleSelect } from 'https://cdn.jsdelivr.net/npm/multiple-select-vanilla@5.1.0/+esm';

    window.addEventListener('DOMContentLoaded', () => {
      multipleSelect('.multiple-select', {
        data: [
          {
            value: '<strong style="color: green">Safe HTML value</strong>',
            text: '1. Safe HTML example'
          },
          {
            value: '<img src="x" onerror="alert(`This should be removed by stripScripts`)">Blocked by stripScripts',
            text: '2. Payload blocked by stripScripts'
          },
          {
            value: '<iframe srcdoc="<script>alert(\'XSS\')\n<\/script>"></iframe>',
            text: '3. Payload that bypasses stripScripts and executes'
          }
        ],
        filter: true,
        placeholder: 'Select an option',
        useSelectOptionLabelToHtml: true
        // No sanitizer is configured.
      });
    });
  </script>
</head>
<body>
  <select class="multiple-select" multiple></select>
</body>
</html>

Steps:

  1. Save the file as an HTML file and open it in a browser.
  2. Open the select dropdown.
  3. Select 1. Safe HTML example. The main choice button renders the safe <strong> HTML value.
  4. Select 2. Payload blocked by stripScripts. The selected value contains an onerror event handler. stripScripts() removes the event-handler payload, so this example demonstrates a case that is successfully filtered by the current regex.
  5. Select 3. Payload that bypasses stripScripts and executes. The selected value contains an iframe srcdoc payload. The value is passed through stripScripts(), but this payload is not reliably removed by the regex-based filter and can execute script when inserted through innerHTML.

Observed result:

The reproduction shows three outcomes in the same configuration:

  1. safe HTML is rendered as intended;
  2. a simple event-handler payload is removed by stripScripts();
  3. a different HTML payload bypasses stripScripts() and can execute script.

This demonstrates that stripScripts() is not a reliable security boundary even though it blocks some common payloads.

Notes:

  • The issue is not present in the default text-only rendering mode.
  • The issue is mitigated when users provide a strict sanitizer such as DOMPurify.
  • The concern is that enabling HTML rendering without a sanitizer is unsafe, and stripScripts() may give a false impression of protection because it is a regex blacklist and can be bypassed.

Expectation

Possible fixes or mitigations:

  • Do not use innerHTML unless a sanitizer is explicitly provided.
  • Make sanitizer required when renderOptionLabelAsHtml or useSelectOptionLabelToHtml is enabled.
  • Remove stripScripts() as a security boundary, or document clearly that it is not a sanitizer.
  • Consider warning in development mode when HTML rendering is enabled without sanitizer.

Environment Info

System:
  OS: Windows 11 10.0.26200
  CPU: (8) x64 Intel(R) Core(TM) i5-10300H CPU @ 2.50GHz
  Memory: 64 GB
Binaries:
  Node: 24.11.0
  npm: 11.6.1
Browsers:
  Edge: Chromium (134.0.3124.51)
npmPackages:
  multiple-select-vanilla: 5.1.0

Validations

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions