VYPR
Medium severity5.4OSV Advisory· Published Nov 25, 2024· Updated Apr 15, 2026

CVE-2024-32468

CVE-2024-32468

Description

Deno is a runtime for JavaScript and TypeScript written in rust. Several cross-site scripting vulnerabilities existed in the deno_doc crate which lead to Self-XSS with deno doc --html. 1.) XSS in generated search_index.js, deno_doc outputs a JavaScript file for searching. However, the generated file used innerHTML on unsanitzed HTML input. 2.) XSS via property, method and enum names, deno_doc did not sanitize property names, method names and enum names. The first XSS most likely didn't have an impact since deno doc --html is expected to be used locally with own packages.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Multiple cross-site scripting vulnerabilities in deno_doc crate allow self-XSS when generating HTML documentation via `deno doc --html`.

Vulnerability

Overview

CVE-2024-32468 describes two cross-site scripting (XSS) vulnerabilities in the deno_doc crate, which is used by the Deno runtime to generate HTML documentation from JavaScript and TypeScript source code. The first issue occurs in the generated search_index.js file, where the code uses innerHTML to insert unsanitized HTML input, allowing injection of arbitrary scripts. The second issue stems from a lack of sanitization of property names, method names, and enum names when rendering documentation pages. Both vulnerabilities stem from insufficient output encoding [1][2].

Exploitation

Prerequisites

Exploitation requires a user to run deno doc --html on a package that contains maliciously crafted identifiers or documentation content. Since deno doc --html is typically used locally with own packages, the attack vector is limited to self-XSS scenarios where an attacker tricks a user into generating documentation for a malicious package. The search_index.js file directly embeds user-controlled data without escaping, as seen in the reference to the template file [3].

Impact

An attacker who successfully exploits these vulnerabilities can execute arbitrary JavaScript in the context of the generated HTML documentation page. This could lead to theft of local data, session hijacking, or other actions performed with the user's privileges. However, the practical impact is reduced because the attack requires the user to generate documentation for a malicious package, making it a self-XSS vulnerability [2].

Mitigation

The vulnerabilities have been addressed in a commit to the deno_doc repository by applying proper HTML escaping via html_escape::encode_safe and removing unsafe innerHTML assignments [1]. Users should update to the latest version of deno_doc that includes this fix. No workarounds are documented, and the issue is not listed in CISA's Known Exploited Vulnerabilities catalog.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
deno_doccrates.io
< 0.119.00.119.0

Affected products

4

Patches

1
0f1ef3efbf16

feat: fix various minor issues & improve inferring (#533)

https://github.com/denoland/deno_docLeo KettmeirMar 27, 2024via ghsa
35 files changed · +1307 100
  • src/html/mod.rs+1 1 modified
    @@ -221,7 +221,7 @@ impl core::ops::Deref for DocNodeWithContext {
     
     pub fn setup_hbs<'t>() -> Result<Handlebars<'t>, anyhow::Error> {
       let mut reg = Handlebars::new();
    -  reg.register_escape_fn(|str| html_escape::encode_safe(str).to_string());
    +  reg.register_escape_fn(|str| html_escape::encode_safe(str).into_owned());
       reg.set_strict_mode(true);
     
       #[cfg(debug_assertions)]
    
  • src/html/parameters.rs+1 4 modified
    @@ -19,7 +19,6 @@ pub(crate) fn render_params(
     
         format!("<span>{items}</span>")
       } else {
    -    // TODO(bartlomieju): refactor to use template
         let mut items = Vec::with_capacity(params.len());
     
         for (i, def) in params.iter().enumerate() {
    @@ -63,13 +62,11 @@ pub(crate) fn param_name(param: &ParamDef, i: usize) -> (String, String) {
         ),
         ParamPatternDef::Assign { left, .. } => param_name(left, i),
         ParamPatternDef::Identifier { name, .. } => {
    -      (html_escape::encode_safe(name).to_string(), name.clone())
    +      (html_escape::encode_safe(name).into_owned(), name.clone())
         }
         ParamPatternDef::Rest { arg } => (
           format!("<span>...{}</span>", param_name(arg, i).0),
           format!("(...{})", param_name(arg, i).1),
         ),
       }
     }
    -
    -// TODO: classes: italic
    
  • src/html/search.rs+21 10 modified
    @@ -36,9 +36,13 @@ fn doc_node_into_search_index_nodes(
         .ns_qualifiers
         .is_empty()
       {
    -    name.to_string()
    +    html_escape::encode_safe(name).into_owned()
       } else {
    -    format!("{}.{}", doc_nodes[0].ns_qualifiers.join("."), name)
    +    format!(
    +      "{}.{}",
    +      doc_nodes[0].ns_qualifiers.join("."),
    +      html_escape::encode_safe(name)
    +    )
       };
     
       if doc_nodes[0].kind != DocNodeKind::Namespace {
    @@ -50,15 +54,16 @@ fn doc_node_into_search_index_nodes(
           .map(|main_entrypoint| main_entrypoint != &location_url)
           .unwrap_or(true)
         {
    -      ctx.url_to_short_path(&location_url).to_name()
    +      html_escape::encode_safe(&ctx.url_to_short_path(&location_url).to_name())
    +        .into_owned()
         } else {
           String::new()
         };
     
         return vec![SearchIndexNode {
           kind: kinds,
           name,
    -      file: doc_nodes[0].origin.as_str().to_string(),
    +      file: html_escape::encode_safe(doc_nodes[0].origin.as_str()).into_owned(),
           location,
           declaration_kind: doc_nodes[0].declaration_kind,
           deprecated,
    @@ -77,15 +82,16 @@ fn doc_node_into_search_index_nodes(
         .map(|main_entrypoint| main_entrypoint != &location_url)
         .unwrap_or(true)
       {
    -    ctx.url_to_short_path(&location_url).to_name()
    +    html_escape::encode_safe(&ctx.url_to_short_path(&location_url).to_name())
    +      .into_owned()
       } else {
         String::new()
       };
     
       nodes.push(SearchIndexNode {
         kind: kinds,
         name,
    -    file: doc_nodes[0].origin.as_str().to_string(),
    +    file: html_escape::encode_safe(doc_nodes[0].origin.as_str()).into_owned(),
         location,
         declaration_kind: doc_nodes[0].declaration_kind,
         deprecated,
    @@ -117,23 +123,28 @@ fn doc_node_into_search_index_nodes(
           .map(|main_entrypoint| main_entrypoint != &location_url)
           .unwrap_or(true)
         {
    -      ctx.url_to_short_path(&location_url).to_name()
    +      html_escape::encode_safe(&ctx.url_to_short_path(&location_url).to_name())
    +        .into_owned()
         } else {
           String::new()
         };
     
         let name = if ns_qualifiers_.is_empty() {
    -      el_name.to_string()
    +      html_escape::encode_safe(el_name).into_owned()
         } else {
    -      format!("{}.{}", ns_qualifiers_.join("."), el_name)
    +      format!(
    +        "{}.{}",
    +        ns_qualifiers_.join("."),
    +        html_escape::encode_safe(el_name)
    +      )
         };
     
         let kinds = el_nodes.iter().map(|node| node.kind).collect();
     
         nodes.push(SearchIndexNode {
           kind: kinds,
           name,
    -      file: doc_nodes[0].origin.as_str().to_string(),
    +      file: html_escape::encode_safe(doc_nodes[0].origin.as_str()).into_owned(),
           location,
           declaration_kind: el_nodes[0].declaration_kind,
           deprecated,
    
  • src/html/symbols/class.rs+5 4 modified
    @@ -42,6 +42,7 @@ pub(crate) fn render_class(
     
       if let Some(type_params) = crate::html::types::render_type_params(
         ctx,
    +    &doc_node.js_doc,
         &class_def.type_params,
         &doc_node.location,
       ) {
    @@ -127,7 +128,7 @@ fn render_constructors(
           DocEntryCtx::new(
             ctx,
             &id,
    -        name,
    +        &html_escape::encode_safe(&name),
             None,
             &format!("({params})"),
             HashSet::from([Tag::New]),
    @@ -379,7 +380,7 @@ fn render_class_accessor(
       DocEntryCtx::new(
         ctx,
         &id,
    -    name,
    +    &html_escape::encode_safe(&name),
         ctx.lookup_symbol_href(&qualify_drilldown_name(
           class_name,
           name,
    @@ -418,7 +419,7 @@ fn render_class_method(
       Some(DocEntryCtx::new(
         ctx,
         &id,
    -    &method.name,
    +    &html_escape::encode_safe(&method.name),
         ctx.lookup_symbol_href(&qualify_drilldown_name(
           class_name,
           &method.name,
    @@ -461,7 +462,7 @@ fn render_class_property(
       DocEntryCtx::new(
         ctx,
         &id,
    -    &property.name,
    +    &html_escape::encode_safe(&property.name),
         ctx.lookup_symbol_href(&qualify_drilldown_name(
           class_name,
           &property.name,
    
  • src/html/symbols/enum.rs+1 1 modified
    @@ -24,7 +24,7 @@ pub(crate) fn render_enum(
           DocEntryCtx::new(
             render_ctx,
             &id,
    -        &member.name,
    +        &html_escape::encode_safe(&member.name),
             None,
             &member
               .init
    
  • src/html/symbols/function.rs+6 6 modified
    @@ -4,7 +4,6 @@ use crate::html::parameters::render_params;
     use crate::html::render_context::RenderContext;
     use crate::html::types::render_type_def;
     use crate::html::types::render_type_def_colon;
    -use crate::html::types::render_type_params;
     use crate::html::types::type_params_summary;
     use crate::html::util::*;
     use crate::html::DocNodeWithContext;
    @@ -154,8 +153,6 @@ fn render_single_function(
         .collect::<HashSet<&str>>();
       let ctx = &ctx.with_current_type_params(current_type_params);
     
    -  // TODO: tags
    -
       let param_docs =
         doc_node
           .js_doc
    @@ -251,9 +248,12 @@ fn render_single_function(
         sections.push(examples);
       }
     
    -  if let Some(type_params) =
    -    render_type_params(ctx, &function_def.type_params, &doc_node.location)
    -  {
    +  if let Some(type_params) = crate::html::types::render_type_params(
    +    ctx,
    +    &doc_node.js_doc,
    +    &function_def.type_params,
    +    &doc_node.location,
    +  ) {
         sections.push(type_params);
       }
     
    
  • src/html/symbols/interface.rs+10 8 modified
    @@ -2,7 +2,6 @@ use crate::html::parameters::render_params;
     use crate::html::render_context::RenderContext;
     use crate::html::symbols::class::IndexSignatureCtx;
     use crate::html::types::render_type_def_colon;
    -use crate::html::types::render_type_params;
     use crate::html::types::type_params_summary;
     use crate::html::util::*;
     use crate::html::DocNodeWithContext;
    @@ -22,9 +21,12 @@ pub(crate) fn render_interface(
     
       let mut sections = vec![];
     
    -  if let Some(type_params) =
    -    render_type_params(ctx, &interface_def.type_params, &doc_node.location)
    -  {
    +  if let Some(type_params) = crate::html::types::render_type_params(
    +    ctx,
    +    &doc_node.js_doc,
    +    &interface_def.type_params,
    +    &doc_node.location,
    +  ) {
         sections.push(type_params);
       }
     
    @@ -185,9 +187,9 @@ fn render_properties(
             ctx,
             &id,
             &if property.computed {
    -          format!("[{}]", property.name)
    +          format!("[{}]", html_escape::encode_safe(&property.name))
             } else {
    -          property.name.clone()
    +          html_escape::encode_safe(&property.name).into_owned()
             },
             ctx.lookup_symbol_href(&qualify_drilldown_name(
               interface_name,
    @@ -226,9 +228,9 @@ fn render_methods(
           let name = if method.name == "new" {
             "<span>new</span>".to_string()
           } else if method.computed {
    -        format!("[{}]", method.name)
    +        format!("[{}]", html_escape::encode_safe(&method.name))
           } else {
    -        method.name.clone()
    +        html_escape::encode_safe(&method.name).into_owned()
           };
     
           let return_type = method
    
  • src/html/symbols/mod.rs+1 1 modified
    @@ -200,7 +200,7 @@ impl DocBlockSubtitleCtx {
           if let Some(extends) = class_def.extends.as_ref() {
             class_extends = Some(DocBlockClassSubtitleExtendsCtx {
               href: ctx.lookup_symbol_href(extends),
    -          symbol: extends.to_owned(),
    +          symbol: html_escape::encode_safe(extends).into_owned(),
               type_args: super::types::type_arguments(
                 ctx,
                 &class_def.super_type_params,
    
  • src/html/symbols/type_alias.rs+2 1 modified
    @@ -19,12 +19,13 @@ pub(crate) fn render_type_alias(
     
       let id = name_to_id("typeAlias", doc_node.get_name());
     
    -  // TODO: tags, TypeParamsDoc
    +  // TODO: tags
     
       let mut sections = vec![];
     
       if let Some(type_params) = crate::html::types::render_type_params(
         ctx,
    +    &doc_node.js_doc,
         &type_alias_def.type_params,
         &doc_node.location,
       ) {
    
  • src/html/templates/deprecated.hbs+1 1 modified
    @@ -8,7 +8,7 @@
     
         {{~#if (ne this "")~}}
           <div class="ml-1 pl-2 border-l-4 border-red-300">
    -        {{{this}}}
    +        {{{this}}} {{! markdown rendering }}
           </div>
         {{~/if~}}
       </div>
    
  • src/html/templates/doc_block_subtitle_class.hbs+4 4 modified
    @@ -2,7 +2,7 @@
       <div>
         <span class="text-stone-400 italic"> implements </span>
         {{~#each implements~}}
    -      {{{~this~}}}
    +      {{{~this~}}} {{! typedef rendering }}
           {{~#unless @last~}}
             <span>, </span>
           {{~/unless~}}
    @@ -15,12 +15,12 @@
         <span class="text-stone-400 italic"> extends </span>
         {{~#if href ~}}
           <a class="link" href="{{href}}">
    -        {{{~symbol~}}}
    +        {{~symbol~}}
           </a>
         {{~else~}}
    -      <span>{{{symbol}}}</span>
    +      <span>{{symbol}}</span>
         {{~/if~}}
    -    <span>{{{type_args}}}</span>
    +    <span>{{{type_args}}} {{! typedef rendering }}</span>
       </div>
     {{~else~}}
     {{~/with~}}
    
  • src/html/templates/doc_block_subtitle_interface.hbs+1 1 modified
    @@ -1,7 +1,7 @@
     <div>
       <span class="text-stone-400 italic"> extends </span>
       {{~#each extends~}}
    -    {{{~this~}}}
    +    {{{~this~}}} {{! typedef rendering }}
         {{~#unless @last~}}
           <span>, </span>
         {{~/unless~}}
    
  • src/html/templates/doc_entry.hbs+2 2 modified
    @@ -13,7 +13,7 @@
     
           <code>
             {{~#if name_href~}}
    -          <a class="font-bold link" href="{{{name_href}}}">{{{name}}}</a>
    +          <a class="font-bold link" href="{{name_href}}">{{{name}}}</a>
             {{~else~}}
               <span class="font-bold">{{{name}}}</span>
             {{~/if~}}
    @@ -28,7 +28,7 @@
     
       {{~#if js_doc~}}
         <div class="markdown_border">
    -      {{{~js_doc~}}}
    +      {{{~js_doc~}}} {{! markdown rendering }}
         </div>
       {{~/if~}}
     </div>
    
  • src/html/templates/example.hbs+2 2 modified
    @@ -4,10 +4,10 @@
       <details id="{{id}}" class="group" open>
         <summary class="list-none flex items-center gap-2 py-2 rounded-lg w-full leading-6 cursor-pointer">
           <div class="text-stone-600 group-open:rotate-90 select-none">&#x25B6;</div>
    -      {{{~markdown_title~}}}
    +      {{{~markdown_title~}}} {{! markdown rendering }}
         </summary>
         <div class="!ml-2 markdown_border">
    -      {{{~markdown_body~}}}
    +      {{{~markdown_body~}}} {{! markdown rendering }}
         </div>
       </details>
     </div>
    
  • src/html/templates/function.hbs+3 3 modified
    @@ -1,6 +1,6 @@
     {{~#each overloads_ctx~}}
       <style scoped>
    -    {{{~this.additional_css~}}}
    +    {{{~this.additional_css~}}} {{! css }}
       </style>
       <input type="radio" name="{{this.function_id}}" id="{{this.overload_id}}" {{this.html_attrs}} />
     {{~/each~}}
    @@ -10,11 +10,11 @@
         <label for="{{this.overload_id}}" class="space-y-1 block px-4 py-2.5 rounded-lg border border-stone-300 cursor-pointer hover:bg-stone-100">
           {{~> deprecated this.deprecated ~}}
           <code class="text-sm">
    -        <span class="font-bold">{{this.name}}</span><span class="font-medium">{{{this.summary}}}</span>
    +        <span class="font-bold">{{this.name}}</span><span class="font-medium">{{{this.summary}}} {{! typedef rendering }}</span>
           </code>
           {{~#if this.summary_doc~}}
             <div class="markdown_border">
    -          {{{~this.summary_doc~}}}
    +          {{{~this.summary_doc~}}} {{! markdown rendering }}
             </div>
           {{~/if~}}
         </label>
    
  • src/html/templates/index_signature.hbs+2 2 modified
    @@ -3,6 +3,6 @@
       {{~#if readonly~}}
         <span>readonly </span>
       {{~/if~}}
    -  [{{{params}}}]
    -  {{{~ts_type~}}}
    +  [{{{params}}}] {{! param rendering }}
    +  {{{~ts_type~}}} {{! typedef rendering }}
     </div>
    
  • src/html/templates/module_doc.hbs+1 1 modified
    @@ -16,7 +16,7 @@
     
       {{~#if toc~}}
         <nav class="flex-none max-w-52 text-sm max-lg:hidden sticky top-0 py-4 max-h-screen box-border">
    -      {{{~toc~}}}
    +      {{{~toc~}}} {{! markdown rendering ToC }}
         </nav>
       {{~/if~}}
     </section>
    
  • src/html/templates/namespace_section.hbs+1 1 modified
    @@ -24,7 +24,7 @@
           </div>
           <div class="block py-1 text-sm leading-5 text-stone-600 !ml-2 markdown_border">
             {{~#if this.docs~}}
    -          {{{~this.docs~}}}
    +          {{{~this.docs~}}} {{! markdown rendering }}
             {{~else~}}
               <span class="italic">No documentation available</span>
             {{~/if~}}
    
  • src/html/templates/symbol_content.hbs+1 1 modified
    @@ -1,6 +1,6 @@
     <div class="space-y-7" id="{{id}}">
       {{~#if docs~}}
    -    {{{~docs~}}}
    +    {{{~docs~}}} {{! markdown rendering }}
       {{~/if~}}
     
       {{~#each sections~}}
    
  • src/html/templates/usages.hbs+2 2 modified
    @@ -1,7 +1,7 @@
     <div class="p-4 mb-4 border-2 rounded-lg bg-stone-50 border-stone-200 usage">
       {{~#each usages~}}
         <style scoped>
    -      {{{~additional_css~}}}
    +      {{{~additional_css~}}} {{! css }}
         </style>
     
         <input type="radio" name="usage" id="{{name}}" class="hidden" {{~#if @first~}}checked{{~/if~}} />
    @@ -18,7 +18,7 @@
       <div>
         {{~#each usages~}}
           <div id="{{name}}_content">
    -        {{{~content~}}}
    +        {{{~content~}}} {{! markdown rendering }}
           </div>
         {{~/each~}}
       </div>
    
  • src/html/types.rs+30 3 modified
    @@ -3,6 +3,8 @@ use super::render_context::RenderContext;
     use super::util::*;
     use std::collections::HashSet;
     
    +use crate::js_doc::JsDoc;
    +use crate::js_doc::JsDocTag;
     use crate::ts_type::LiteralDefKind;
     use crate::ts_type::TsTypeDefKind;
     use crate::ts_type_param::TsTypeParamDef;
    @@ -124,7 +126,19 @@ pub(crate) fn render_type_def(
         TsTypeDefKind::Optional => {
           render_type_def(ctx, def.optional.as_ref().unwrap())
         }
    -    TsTypeDefKind::TypeQuery => def.type_query.clone().unwrap(),
    +    TsTypeDefKind::TypeQuery => {
    +      let query = def.type_query.as_ref().unwrap();
    +
    +      if let Some(href) = ctx.lookup_symbol_href(query) {
    +        format!(
    +          r#"<a href="{}" class="link">{}</a>"#,
    +          html_escape::encode_safe(&href),
    +          html_escape::encode_safe(query),
    +        )
    +      } else {
    +        format!("<span>{}</span>", html_escape::encode_safe(query))
    +      }
    +    }
         TsTypeDefKind::This => "<span>this</span>".to_string(),
         TsTypeDefKind::FnOrConstructor => {
           let fn_or_constructor = def.fn_or_constructor.as_ref().unwrap();
    @@ -496,6 +510,7 @@ pub(crate) fn type_arguments(
     
     pub(crate) fn render_type_params(
       ctx: &RenderContext,
    +  js_doc: &JsDoc,
       type_params: &[TsTypeParamDef],
       location: &crate::Location,
     ) -> Option<SectionCtx> {
    @@ -505,6 +520,18 @@ pub(crate) fn render_type_params(
     
       let mut items = Vec::with_capacity(type_params.len());
     
    +  let type_param_docs = js_doc
    +    .tags
    +    .iter()
    +    .filter_map(|tag| {
    +      if let JsDocTag::Template { name, doc } = tag {
    +        doc.as_ref().map(|doc| (name.as_str(), doc.as_str()))
    +      } else {
    +        None
    +      }
    +    })
    +    .collect::<std::collections::HashMap<&str, &str>>();
    +
       for type_param in type_params.iter() {
         let id = name_to_id("type_param", &type_param.name);
     
    @@ -533,11 +560,11 @@ pub(crate) fn render_type_params(
         let content = DocEntryCtx::new(
           ctx,
           &id,
    -      &type_param.name,
    +      &html_escape::encode_safe(&type_param.name),
           None,
           &format!("{constraint}{default}"),
           HashSet::new(),
    -      None,
    +      type_param_docs.get(type_param.name.as_str()).cloned(),
           location,
         );
     
    
  • src/html/usage.rs+9 6 modified
    @@ -62,20 +62,23 @@ pub fn usage_to_md(
     
         let mut usage_statement = if is_default {
           format!(
    -        r#"import {}{import_symbol} from "{url}";"#,
    -        if is_type { "type " } else { "" }
    +        r#"import {}{} from "{url}";"#,
    +        if is_type { "type " } else { "" },
    +        html_escape::encode_safe(&import_symbol),
           )
         } else {
           format!(
    -        r#"import {{ {}{import_symbol} }} from "{url}";"#,
    -        if is_type { "type " } else { "" }
    +        r#"import {{ {}{} }} from "{url}";"#,
    +        if is_type { "type " } else { "" },
    +        html_escape::encode_safe(&import_symbol),
           )
         };
     
         if let Some((usage_symbol, local_var)) = usage_symbol {
           usage_statement.push_str(&format!(
    -        "\n{} {{ {usage_symbol} }} = {local_var};",
    -        if is_type { "type" } else { "const" }
    +        "\n{} {{ {} }} = {local_var};",
    +        if is_type { "type" } else { "const" },
    +        html_escape::encode_safe(usage_symbol),
           ));
         }
     
    
  • src/html/util.rs+4 1 modified
    @@ -18,7 +18,10 @@ lazy_static! {
     }
     
     pub(crate) fn name_to_id(kind: &str, name: &str) -> String {
    -  format!("{kind}_{}", TARGET_RE.replace_all(name, "_"))
    +  format!(
    +    "{kind}_{}",
    +    html_escape::encode_safe(&TARGET_RE.replace_all(name, "_"))
    +  )
     }
     
     /// A container to hold a list of symbols with their namespaces:
    
  • src/js_doc.rs+4 2 modified
    @@ -7,7 +7,7 @@ use serde::Serialize;
     lazy_static! {
       static ref JS_DOC_TAG_MAYBE_DOC_RE: Regex = Regex::new(r"(?s)^\s*@(deprecated)(?:\s+(.+))?").unwrap();
       static ref JS_DOC_TAG_DOC_RE: Regex = Regex::new(r"(?s)^\s*@(category|see|example|tags)(?:\s+(.+))").unwrap();
    -  static ref JS_DOC_TAG_NAMED_RE: Regex = Regex::new(r"(?s)^\s*@(callback|template)\s+([a-zA-Z_$]\S*)(?:\s+(.+))?").unwrap();
    +  static ref JS_DOC_TAG_NAMED_RE: Regex = Regex::new(r"(?s)^\s*@(callback|template|typeparam|typeParam)\s+([a-zA-Z_$]\S*)(?:\s+(.+))?").unwrap();
       static ref JS_DOC_TAG_NAMED_TYPED_RE: Regex = Regex::new(r"(?s)^\s*@(prop(?:erty)?|typedef)\s+\{([^}]+)\}\s+([a-zA-Z_$]\S*)(?:\s+(.+))?").unwrap();
       static ref JS_DOC_TAG_ONLY_RE: Regex = Regex::new(r"^\s*@(constructor|class|ignore|module|public|private|protected|readonly)").unwrap();
       static ref JS_DOC_TAG_PARAM_RE: Regex = Regex::new(
    @@ -168,6 +168,8 @@ pub enum JsDocTag {
         tags: Vec<String>,
       },
       /// `@template T comment`
    +  /// `@typeparam T comment`
    +  /// `@typeParam T comment`
       Template {
         name: String,
         #[serde(skip_serializing_if = "Option::is_none", default)]
    @@ -225,7 +227,7 @@ impl From<String> for JsDocTag {
           let doc = caps.get(3).map(|m| m.as_str().to_string());
           match kind {
             "callback" => Self::Callback { name, doc },
    -        "template" => Self::Template { name, doc },
    +        "template" | "typeparam" | "typeParam" => Self::Template { name, doc },
             _ => unreachable!("kind unexpected: {}", kind),
           }
         } else if let Some(caps) = JS_DOC_TAG_TYPED_RE.captures(&value) {
    
  • src/ts_type.rs+122 2 modified
    @@ -1380,7 +1380,126 @@ pub fn infer_ts_type_from_expr(
           // e.g.) const value = {foo: "bar"};
           infer_ts_type_from_obj(parsed_source, obj)
         }
    -    _ => None,
    +    Expr::TsSatisfies(satisfies) => {
    +      // e.g.) const value = {foo: "bar"} satifies Record<string, string>;
    +      infer_ts_type_from_expr(parsed_source, &satisfies.expr, is_const)
    +    }
    +    Expr::Update(_) => {
    +      // e.g.) let foo = 0;
    +      //       const bar = foo++;
    +      Some(TsTypeDef::number_with_repr("number"))
    +    }
    +    Expr::TsTypeAssertion(assertion) => {
    +      // e.g.) export const foo = <string> 1;
    +      Some(TsTypeDef::new(parsed_source, &assertion.type_ann))
    +    }
    +    Expr::TsAs(as_expr) => {
    +      // e.g.) export const foo = 1 as string;
    +      Some(TsTypeDef::new(parsed_source, &as_expr.type_ann))
    +    }
    +    Expr::Paren(paren) => {
    +      // e.g.) export const foo = (1);
    +      infer_ts_type_from_expr(parsed_source, &paren.expr, is_const)
    +    }
    +    Expr::Await(await_expr) => {
    +      // e.g.) export const foo = await 1;
    +      infer_ts_type_from_expr(parsed_source, &await_expr.arg, is_const)
    +    }
    +    Expr::Cond(cond) => {
    +      // e.g.) export const foo = true ? "a" : 1;
    +      let left = infer_ts_type_from_expr(parsed_source, &cond.cons, is_const)?;
    +      let right = infer_ts_type_from_expr(parsed_source, &cond.alt, is_const)?;
    +
    +      Some(TsTypeDef {
    +        union: Some(vec![left, right]),
    +        kind: Some(TsTypeDefKind::Union),
    +        ..Default::default()
    +      })
    +    }
    +    Expr::TsNonNull(non_null) => {
    +      // e.g.) export const foo = (true ? "a" : null)!;
    +      // e.g.) export const foo = null!;
    +      let with_null =
    +        infer_ts_type_from_expr(parsed_source, &non_null.expr, is_const)?;
    +
    +      if let Some(union) = with_null.union {
    +        let mut non_null_union = union
    +          .into_iter()
    +          .filter(|item| {
    +            if let Some(keyword) = &item.keyword {
    +              return keyword != "null";
    +            }
    +
    +            true
    +          })
    +          .collect::<Vec<_>>();
    +
    +        Some(match non_null_union.len() {
    +          0 => TsTypeDef::keyword("never"),
    +          1 => non_null_union.remove(0),
    +          _ => TsTypeDef {
    +            union: Some(non_null_union),
    +            kind: Some(TsTypeDefKind::Union),
    +            ..Default::default()
    +          },
    +        })
    +      } else if with_null.keyword.is_some_and(|keyword| keyword == "null") {
    +        Some(TsTypeDef::keyword("never"))
    +      } else {
    +        None
    +      }
    +    }
    +    Expr::Bin(bin) => {
    +      // e.g.) export const foo = 1 == "bar";
    +      // e.g.) export const foo = 1 >> 1;
    +      match bin.op {
    +        BinaryOp::EqEq
    +        | BinaryOp::NotEq
    +        | BinaryOp::EqEqEq
    +        | BinaryOp::NotEqEq
    +        | BinaryOp::Lt
    +        | BinaryOp::LtEq
    +        | BinaryOp::Gt
    +        | BinaryOp::GtEq
    +        | BinaryOp::In
    +        | BinaryOp::InstanceOf => Some(TsTypeDef::bool_with_repr("boolean")),
    +        BinaryOp::LShift
    +        | BinaryOp::RShift
    +        | BinaryOp::ZeroFillRShift
    +        | BinaryOp::Sub
    +        | BinaryOp::Mul
    +        | BinaryOp::Div
    +        | BinaryOp::Mod
    +        | BinaryOp::BitOr
    +        | BinaryOp::BitXor
    +        | BinaryOp::BitAnd
    +        | BinaryOp::Exp => Some(TsTypeDef::number_with_repr("number")),
    +        BinaryOp::LogicalOr
    +        | BinaryOp::LogicalAnd
    +        | BinaryOp::NullishCoalescing
    +        | BinaryOp::Add => None,
    +      }
    +    }
    +    Expr::This(_)
    +    | Expr::Unary(_)
    +    | Expr::Assign(_)
    +    | Expr::Member(_)
    +    | Expr::SuperProp(_)
    +    | Expr::Seq(_)
    +    | Expr::Ident(_)
    +    | Expr::TaggedTpl(_)
    +    | Expr::Class(_)
    +    | Expr::Yield(_)
    +    | Expr::MetaProp(_)
    +    | Expr::JSXMember(_)
    +    | Expr::JSXNamespacedName(_)
    +    | Expr::JSXEmpty(_)
    +    | Expr::JSXElement(_)
    +    | Expr::JSXFragment(_)
    +    | Expr::TsInstantiation(_)
    +    | Expr::PrivateName(_)
    +    | Expr::OptChain(_)
    +    | Expr::Invalid(_) => None,
       }
     }
     
    @@ -1524,7 +1643,8 @@ fn infer_ts_type_from_lit(lit: &Lit, is_const: bool) -> Option<TsTypeDef> {
           }
         }
         Lit::Regex(regex) => Some(TsTypeDef::regexp(regex.exp.to_string())),
    -    _ => None,
    +    Lit::Null(_null) => Some(TsTypeDef::keyword("null")),
    +    Lit::JSXText(_) => None,
       }
     }
     
    
  • src/variable.rs+3 5 modified
    @@ -82,23 +82,21 @@ pub fn get_docs_for_var_declarator(
         });
     
       match &var_declarator.name {
    -    deno_ast::swc::ast::Pat::Ident(ident) => {
    +    Pat::Ident(ident) => {
           let var_name = ident.id.sym.to_string();
           let variable_def = VariableDef {
             ts_type: maybe_ts_type,
             kind: var_decl.kind,
           };
           items.push((var_name, variable_def, Some(var_declarator.range())));
         }
    -    deno_ast::swc::ast::Pat::Object(pat) => {
    +    Pat::Object(pat) => {
           for prop in &pat.props {
             let (name, reassign_name, maybe_range) = match prop {
               deno_ast::swc::ast::ObjectPatProp::KeyValue(kv) => (
                 crate::params::prop_name_to_string(module_info.source(), &kv.key),
                 match &*kv.value {
    -              deno_ast::swc::ast::Pat::Ident(ident) => {
    -                Some(ident.sym.to_string())
    -              }
    +              Pat::Ident(ident) => Some(ident.sym.to_string()),
                   _ => None, // TODO: properly implement
                 },
                 None,
    
  • tests/html_test.rs+1 0 modified
    @@ -213,6 +213,7 @@ async fn html_doc_files_rewrite() {
           "./~/Bar.prototype.html",
           "./~/Foo.bar.html",
           "./~/Foo.html",
    +      "./~/Foo.prototype.\"><img src=x onerror=alert(1)>.html",
           "./~/Foo.prototype.foo.html",
           "./~/Foo.prototype.html",
           "./~/Foobar.html",
    
  • tests/specs/InferObjectLiteralSatifies.txt+189 0 added
    @@ -0,0 +1,189 @@
    +# mod.ts
    +export type Colors = "red" | "green" | "blue";
    +
    +// Ensure that we have exactly the keys from 'Colors'.
    +export const favoriteColors = {
    +    "red": "yes",
    +    "green": false,
    +    "blue": "kinda",
    +    "platypus": false
    +//  ~~~~~~~~~~ error - "platypus" was never listed in 'Colors'.
    +} satisfies Record<Colors, unknown>;
    +
    +export const g: boolean = favoriteColors.green;
    +
    +# diagnostics
    +error[missing-jsdoc]: exported symbol is missing JSDoc documentation
    + --> /mod.ts:1:1
    +  | 
    +1 | export type Colors = "red" | "green" | "blue";
    +  | ^
    +
    +
    +error[missing-jsdoc]: exported symbol is missing JSDoc documentation
    + --> /mod.ts:4:14
    +  | 
    +4 | export const favoriteColors = {
    +  |              ^
    +
    +
    +error[missing-jsdoc]: exported symbol is missing JSDoc documentation
    +  --> /mod.ts:12:14
    +   | 
    +12 | export const g: boolean = favoriteColors.green;
    +   |              ^
    +
    +
    +# output.txt
    +Defined in file:///mod.ts:4:14
    +
    +const favoriteColors: { red: string; green: boolean; blue: string; platypus: boolean; }
    +
    +Defined in file:///mod.ts:12:14
    +
    +const g: boolean
    +
    +Defined in file:///mod.ts:1:1
    +
    +type Colors = "red" | "green" | "blue"
    +
    +
    +# output.json
    +[
    +  {
    +    "kind": "typeAlias",
    +    "name": "Colors",
    +    "location": {
    +      "filename": "file:///mod.ts",
    +      "line": 1,
    +      "col": 0,
    +      "byteIndex": 0
    +    },
    +    "declarationKind": "export",
    +    "typeAliasDef": {
    +      "tsType": {
    +        "repr": "",
    +        "kind": "union",
    +        "union": [
    +          {
    +            "repr": "red",
    +            "kind": "literal",
    +            "literal": {
    +              "kind": "string",
    +              "string": "red"
    +            }
    +          },
    +          {
    +            "repr": "green",
    +            "kind": "literal",
    +            "literal": {
    +              "kind": "string",
    +              "string": "green"
    +            }
    +          },
    +          {
    +            "repr": "blue",
    +            "kind": "literal",
    +            "literal": {
    +              "kind": "string",
    +              "string": "blue"
    +            }
    +          }
    +        ]
    +      },
    +      "typeParams": []
    +    }
    +  },
    +  {
    +    "kind": "variable",
    +    "name": "favoriteColors",
    +    "location": {
    +      "filename": "file:///mod.ts",
    +      "line": 4,
    +      "col": 13,
    +      "byteIndex": 116
    +    },
    +    "declarationKind": "export",
    +    "variableDef": {
    +      "tsType": {
    +        "repr": "",
    +        "kind": "typeLiteral",
    +        "typeLiteral": {
    +          "methods": [],
    +          "properties": [
    +            {
    +              "name": "red",
    +              "params": [],
    +              "computed": false,
    +              "optional": false,
    +              "tsType": {
    +                "repr": "string",
    +                "kind": "keyword",
    +                "keyword": "string"
    +              },
    +              "typeParams": []
    +            },
    +            {
    +              "name": "green",
    +              "params": [],
    +              "computed": false,
    +              "optional": false,
    +              "tsType": {
    +                "repr": "boolean",
    +                "kind": "keyword",
    +                "keyword": "boolean"
    +              },
    +              "typeParams": []
    +            },
    +            {
    +              "name": "blue",
    +              "params": [],
    +              "computed": false,
    +              "optional": false,
    +              "tsType": {
    +                "repr": "string",
    +                "kind": "keyword",
    +                "keyword": "string"
    +              },
    +              "typeParams": []
    +            },
    +            {
    +              "name": "platypus",
    +              "params": [],
    +              "computed": false,
    +              "optional": false,
    +              "tsType": {
    +                "repr": "boolean",
    +                "kind": "keyword",
    +                "keyword": "boolean"
    +              },
    +              "typeParams": []
    +            }
    +          ],
    +          "callSignatures": [],
    +          "indexSignatures": []
    +        }
    +      },
    +      "kind": "const"
    +    }
    +  },
    +  {
    +    "kind": "variable",
    +    "name": "g",
    +    "location": {
    +      "filename": "file:///mod.ts",
    +      "line": 12,
    +      "col": 13,
    +      "byteIndex": 331
    +    },
    +    "declarationKind": "export",
    +    "variableDef": {
    +      "tsType": {
    +        "repr": "boolean",
    +        "kind": "keyword",
    +        "keyword": "boolean"
    +      },
    +      "kind": "const"
    +    }
    +  }
    +]
    
  • tests/specs/InferObjectLiteral.txt+21 21 modified
    @@ -1,33 +1,33 @@
     # mod.ts
     const s: symbol = Symbol.for("s");
    -    const t: symbol = Symbol.for("t");
    +const t: symbol = Symbol.for("t");
     
    -    export const a = {
    -      a: "a",
    -      b: new Map<string, number>(),
    -      c: { d: "d" },
    -      d(e: string): void {},
    -      f: (g: string): void => {},
    -      get h(): string {
    -        return "h";
    -      },
    -      set h(value: string) {
    +export const a = {
    +  a: "a",
    +  b: new Map<string, number>(),
    +  c: { d: "d" },
    +  d(e: string): void {},
    +  f: (g: string): void => {},
    +  get h(): string {
    +    return "h";
    +  },
    +  set h(value: string) {
     
    -      },
    -      [s]: [1, 2, 3, "a"],
    -      [t](u: string): void {},
    -    };
    +  },
    +  [s]: [1, 2, 3, "a"],
    +  [t](u: string): void {},
    +};
     
     # diagnostics
     error[missing-jsdoc]: exported symbol is missing JSDoc documentation
    - --> /mod.ts:4:18
    + --> /mod.ts:4:14
       | 
    -4 |     export const a = {
    -  |                  ^
    +4 | export const a = {
    +  |              ^
     
     
     # output.txt
    -Defined in file:///mod.ts:4:18
    +Defined in file:///mod.ts:4:14
     
     const a: { d(e: string): void; h(): string; h(value: string); [[t]](u: string): void; a: string; b: Map<string, number>; c: { d: string; }; f: (g: string) => void; [s]: (number | string)[]; }
     
    @@ -40,8 +40,8 @@ const a: { d(e: string): void; h(): string; h(value: string); [[t]](u: string):
         "location": {
           "filename": "file:///mod.ts",
           "line": 4,
    -      "col": 17,
    -      "byteIndex": 92
    +      "col": 13,
    +      "byteIndex": 84
         },
         "declarationKind": "export",
         "variableDef": {
    
  • tests/specs/InferVariable.txt+341 0 added
    @@ -0,0 +1,341 @@
    +# mod.ts
    +export let a = (0);
    +export const b = a++;
    +export const c = <string> 1;
    +export const d = 1 as string;
    +export const e = await 1;
    +export const f = true ? "a" : 1;
    +export const g = (true ? "a" : null)!;
    +export const h = null!;
    +export const i = 1 == "bar";
    +export const j = 1 >> 1;
    +
    +# diagnostics
    +error[missing-jsdoc]: exported symbol is missing JSDoc documentation
    + --> /mod.ts:1:12
    +  | 
    +1 | export let a = (0);
    +  |            ^
    +
    +
    +error[missing-jsdoc]: exported symbol is missing JSDoc documentation
    + --> /mod.ts:2:14
    +  | 
    +2 | export const b = a++;
    +  |              ^
    +
    +
    +error[missing-jsdoc]: exported symbol is missing JSDoc documentation
    + --> /mod.ts:3:14
    +  | 
    +3 | export const c = <string> 1;
    +  |              ^
    +
    +
    +error[missing-jsdoc]: exported symbol is missing JSDoc documentation
    + --> /mod.ts:4:14
    +  | 
    +4 | export const d = 1 as string;
    +  |              ^
    +
    +
    +error[missing-jsdoc]: exported symbol is missing JSDoc documentation
    + --> /mod.ts:5:14
    +  | 
    +5 | export const e = await 1;
    +  |              ^
    +
    +
    +error[missing-jsdoc]: exported symbol is missing JSDoc documentation
    + --> /mod.ts:6:14
    +  | 
    +6 | export const f = true ? "a" : 1;
    +  |              ^
    +
    +
    +error[missing-jsdoc]: exported symbol is missing JSDoc documentation
    + --> /mod.ts:7:14
    +  | 
    +7 | export const g = (true ? "a" : null)!;
    +  |              ^
    +
    +
    +error[missing-jsdoc]: exported symbol is missing JSDoc documentation
    + --> /mod.ts:8:14
    +  | 
    +8 | export const h = null!;
    +  |              ^
    +
    +
    +error[missing-jsdoc]: exported symbol is missing JSDoc documentation
    + --> /mod.ts:9:14
    +  | 
    +9 | export const i = 1 == "bar";
    +  |              ^
    +
    +
    +error[missing-jsdoc]: exported symbol is missing JSDoc documentation
    +  --> /mod.ts:10:14
    +   | 
    +10 | export const j = 1 >> 1;
    +   |              ^
    +
    +
    +# output.txt
    +Defined in file:///mod.ts:1:12
    +
    +let a: number
    +
    +Defined in file:///mod.ts:2:14
    +
    +const b: number
    +
    +Defined in file:///mod.ts:3:14
    +
    +const c: string
    +
    +Defined in file:///mod.ts:4:14
    +
    +const d: string
    +
    +Defined in file:///mod.ts:5:14
    +
    +const e: 1
    +
    +Defined in file:///mod.ts:6:14
    +
    +const f: "a" | 1
    +
    +Defined in file:///mod.ts:7:14
    +
    +const g: "a"
    +
    +Defined in file:///mod.ts:8:14
    +
    +const h: never
    +
    +Defined in file:///mod.ts:9:14
    +
    +const i: boolean
    +
    +Defined in file:///mod.ts:10:14
    +
    +const j: number
    +
    +
    +# output.json
    +[
    +  {
    +    "kind": "variable",
    +    "name": "a",
    +    "location": {
    +      "filename": "file:///mod.ts",
    +      "line": 1,
    +      "col": 11,
    +      "byteIndex": 11
    +    },
    +    "declarationKind": "export",
    +    "variableDef": {
    +      "tsType": {
    +        "repr": "number",
    +        "kind": "keyword",
    +        "keyword": "number"
    +      },
    +      "kind": "let"
    +    }
    +  },
    +  {
    +    "kind": "variable",
    +    "name": "b",
    +    "location": {
    +      "filename": "file:///mod.ts",
    +      "line": 2,
    +      "col": 13,
    +      "byteIndex": 33
    +    },
    +    "declarationKind": "export",
    +    "variableDef": {
    +      "tsType": {
    +        "repr": "number",
    +        "kind": "keyword",
    +        "keyword": "number"
    +      },
    +      "kind": "const"
    +    }
    +  },
    +  {
    +    "kind": "variable",
    +    "name": "c",
    +    "location": {
    +      "filename": "file:///mod.ts",
    +      "line": 3,
    +      "col": 13,
    +      "byteIndex": 55
    +    },
    +    "declarationKind": "export",
    +    "variableDef": {
    +      "tsType": {
    +        "repr": "string",
    +        "kind": "keyword",
    +        "keyword": "string"
    +      },
    +      "kind": "const"
    +    }
    +  },
    +  {
    +    "kind": "variable",
    +    "name": "d",
    +    "location": {
    +      "filename": "file:///mod.ts",
    +      "line": 4,
    +      "col": 13,
    +      "byteIndex": 84
    +    },
    +    "declarationKind": "export",
    +    "variableDef": {
    +      "tsType": {
    +        "repr": "string",
    +        "kind": "keyword",
    +        "keyword": "string"
    +      },
    +      "kind": "const"
    +    }
    +  },
    +  {
    +    "kind": "variable",
    +    "name": "e",
    +    "location": {
    +      "filename": "file:///mod.ts",
    +      "line": 5,
    +      "col": 13,
    +      "byteIndex": 114
    +    },
    +    "declarationKind": "export",
    +    "variableDef": {
    +      "tsType": {
    +        "repr": "1",
    +        "kind": "literal",
    +        "literal": {
    +          "kind": "number",
    +          "number": 1.0
    +        }
    +      },
    +      "kind": "const"
    +    }
    +  },
    +  {
    +    "kind": "variable",
    +    "name": "f",
    +    "location": {
    +      "filename": "file:///mod.ts",
    +      "line": 6,
    +      "col": 13,
    +      "byteIndex": 140
    +    },
    +    "declarationKind": "export",
    +    "variableDef": {
    +      "tsType": {
    +        "repr": "",
    +        "kind": "union",
    +        "union": [
    +          {
    +            "repr": "a",
    +            "kind": "literal",
    +            "literal": {
    +              "kind": "string",
    +              "string": "a"
    +            }
    +          },
    +          {
    +            "repr": "1",
    +            "kind": "literal",
    +            "literal": {
    +              "kind": "number",
    +              "number": 1.0
    +            }
    +          }
    +        ]
    +      },
    +      "kind": "const"
    +    }
    +  },
    +  {
    +    "kind": "variable",
    +    "name": "g",
    +    "location": {
    +      "filename": "file:///mod.ts",
    +      "line": 7,
    +      "col": 13,
    +      "byteIndex": 173
    +    },
    +    "declarationKind": "export",
    +    "variableDef": {
    +      "tsType": {
    +        "repr": "a",
    +        "kind": "literal",
    +        "literal": {
    +          "kind": "string",
    +          "string": "a"
    +        }
    +      },
    +      "kind": "const"
    +    }
    +  },
    +  {
    +    "kind": "variable",
    +    "name": "h",
    +    "location": {
    +      "filename": "file:///mod.ts",
    +      "line": 8,
    +      "col": 13,
    +      "byteIndex": 212
    +    },
    +    "declarationKind": "export",
    +    "variableDef": {
    +      "tsType": {
    +        "repr": "never",
    +        "kind": "keyword",
    +        "keyword": "never"
    +      },
    +      "kind": "const"
    +    }
    +  },
    +  {
    +    "kind": "variable",
    +    "name": "i",
    +    "location": {
    +      "filename": "file:///mod.ts",
    +      "line": 9,
    +      "col": 13,
    +      "byteIndex": 236
    +    },
    +    "declarationKind": "export",
    +    "variableDef": {
    +      "tsType": {
    +        "repr": "boolean",
    +        "kind": "keyword",
    +        "keyword": "boolean"
    +      },
    +      "kind": "const"
    +    }
    +  },
    +  {
    +    "kind": "variable",
    +    "name": "j",
    +    "location": {
    +      "filename": "file:///mod.ts",
    +      "line": 10,
    +      "col": 13,
    +      "byteIndex": 265
    +    },
    +    "declarationKind": "export",
    +    "variableDef": {
    +      "tsType": {
    +        "repr": "number",
    +        "kind": "keyword",
    +        "keyword": "number"
    +      },
    +      "kind": "const"
    +    }
    +  }
    +]
    
  • tests/testdata/multiple/a.ts+1 0 modified
    @@ -25,6 +25,7 @@
     export class Foo {
       static bar: "string";
       foo: "string";
    +  '"><img src=x onerror=alert(1)>' = 0;
     }
     
     export class Bar extends Foo {
    
  • tests/testdata/symbol_group.json+170 0 modified
    @@ -230,6 +230,18 @@
                         "content": {
                           "kind": "doc_entry",
                           "content": [
    +                        {
    +                          "id": "property_&quot;&gt;&lt;img src=x onerror=alert(1)&gt;",
    +                          "name": "&quot;&gt;&lt;img src=x onerror=alert(1)&gt;",
    +                          "name_href": "../././~/Foo.prototype.\"><img src=x onerror=alert(1)>.html",
    +                          "content": "<span>: <span>number</span></span>",
    +                          "anchor": {
    +                            "id": "property_&quot;&gt;&lt;img src=x onerror=alert(1)&gt;"
    +                          },
    +                          "tags": [],
    +                          "js_doc": null,
    +                          "source_href": null
    +                        },
                             {
                               "id": "property_foo",
                               "name": "foo",
    @@ -585,6 +597,164 @@
           ]
         }
       },
    +  {
    +    "html_head_ctx": {
    +      "title": "Foo.prototype.\"><img src=x onerror=alert(1)> - documentation",
    +      "current_file": ".",
    +      "stylesheet_url": "../styles.css",
    +      "page_stylesheet_url": "../page.css",
    +      "url_search_index": "../search_index.js",
    +      "script_js": "../script.js",
    +      "fuse_js": "../fuse.js",
    +      "url_search": "../search.js"
    +    },
    +    "sidepanel_ctx": {
    +      "package_name": null,
    +      "partitions": [
    +        {
    +          "name": "Class",
    +          "symbols": [
    +            {
    +              "kind": [
    +                {
    +                  "kind": "Class",
    +                  "char": "c",
    +                  "title": "Class",
    +                  "title_lowercase": "class",
    +                  "title_plural": "Classes"
    +                }
    +              ],
    +              "name": "Bar",
    +              "href": "../././~/Bar.html",
    +              "active": false,
    +              "deprecated": false
    +            },
    +            {
    +              "kind": [
    +                {
    +                  "kind": "Class",
    +                  "char": "c",
    +                  "title": "Class",
    +                  "title_lowercase": "class",
    +                  "title_plural": "Classes"
    +                }
    +              ],
    +              "name": "Foo",
    +              "href": "../././~/Foo.html",
    +              "active": false,
    +              "deprecated": false
    +            },
    +            {
    +              "kind": [
    +                {
    +                  "kind": "Class",
    +                  "char": "c",
    +                  "title": "Class",
    +                  "title_lowercase": "class",
    +                  "title_plural": "Classes"
    +                }
    +              ],
    +              "name": "Foobar",
    +              "href": "../././~/Foobar.html",
    +              "active": false,
    +              "deprecated": false
    +            }
    +          ]
    +        }
    +      ]
    +    },
    +    "symbol_group_ctx": {
    +      "name": "Foo.prototype.\"><img src=x onerror=alert(1)>",
    +      "symbols": [
    +        {
    +          "kind": {
    +            "kind": "Property",
    +            "char": " ",
    +            "title": "Property",
    +            "title_lowercase": "property",
    +            "title_plural": "Properties"
    +          },
    +          "tags": [],
    +          "subtitle": null,
    +          "content": [
    +            {
    +              "kind": "other",
    +              "value": {
    +                "id": "",
    +                "docs": null,
    +                "sections": [
    +                  {
    +                    "title": "Type",
    +                    "content": {
    +                      "kind": "doc_entry",
    +                      "content": [
    +                        {
    +                          "id": "variable_Foo_prototype_&quot;&gt;&lt;img src=x onerror=alert(1)&gt;",
    +                          "name": "",
    +                          "name_href": null,
    +                          "content": "<span>number</span>",
    +                          "anchor": {
    +                            "id": "variable_Foo_prototype_&quot;&gt;&lt;img src=x onerror=alert(1)&gt;"
    +                          },
    +                          "tags": [],
    +                          "js_doc": null,
    +                          "source_href": null
    +                        }
    +                      ]
    +                    }
    +                  }
    +                ]
    +              }
    +            }
    +          ],
    +          "deprecated": null,
    +          "source_href": null
    +        }
    +      ],
    +      "usages": {
    +        "show_tabs": false,
    +        "usages": [
    +          {
    +            "name": "",
    +            "content": "<div class=\"markdown flex-1\"><pre class=\"highlight\"><code>import { Foo } from \".\";\nconst { \"&gt;&lt;img src=x onerror=alert(1)&gt; } = Foo.prototype;\n</code><button class=\"context_button\" data-copy=\"import { Foo } from &quot;.&quot;;\nconst { &amp;quot;&amp;gt;&amp;lt;img src=x onerror=alert(1)&amp;gt; } = Foo.prototype;\n\"><svg width=\"15\" height=\"15\" viewBox=\"0 0 15 15\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n  <path d=\"M1.55566 2.7C1.55566 2.03726 2.09292 1.5 2.75566 1.5H8.75566C9.41841 1.5 9.95566 2.03726 9.95566 2.7V5.1H12.3557C13.0184 5.1 13.5557 5.63726 13.5557 6.3V12.3C13.5557 12.9627 13.0184 13.5 12.3557 13.5H6.35566C5.69292 13.5 5.15566 12.9627 5.15566 12.3V9.9H2.75566C2.09292 9.9 1.55566 9.36274 1.55566 8.7V2.7ZM6.35566 9.9V12.3H12.3557V6.3H9.95566V8.7C9.95566 9.36274 9.41841 9.9 8.75566 9.9H6.35566ZM8.75566 8.7V2.7L2.75566 2.7V8.7H8.75566Z\" fill=\"#232323\"></path>\n</svg>\n</button><code></code></pre>\n</div>",
    +            "additional_css": ""
    +          }
    +        ]
    +      }
    +    },
    +    "breadcrumbs_ctx": {
    +      "parts": [
    +        {
    +          "name": "index",
    +          "href": "../",
    +          "is_symbol": false,
    +          "is_first_symbol": false,
    +          "is_all_symbols_part": false
    +        },
    +        {
    +          "name": "Foo",
    +          "href": "../././~/Foo.html",
    +          "is_symbol": true,
    +          "is_first_symbol": true,
    +          "is_all_symbols_part": false
    +        },
    +        {
    +          "name": "prototype",
    +          "href": "../././~/Foo.prototype.html",
    +          "is_symbol": true,
    +          "is_first_symbol": false,
    +          "is_all_symbols_part": false
    +        },
    +        {
    +          "name": "\"><img src=x onerror=alert(1)>",
    +          "href": "../././~/Foo.prototype.\"><img src=x onerror=alert(1)>.html",
    +          "is_symbol": true,
    +          "is_first_symbol": false,
    +          "is_all_symbols_part": false
    +        }
    +      ]
    +    }
    +  },
       {
         "html_head_ctx": {
           "title": "Foo.prototype.foo - documentation",
    
  • tests/testdata/symbol_group-syntect.json+170 0 modified
    @@ -230,6 +230,18 @@
                         "content": {
                           "kind": "doc_entry",
                           "content": [
    +                        {
    +                          "id": "property_&quot;&gt;&lt;img src=x onerror=alert(1)&gt;",
    +                          "name": "&quot;&gt;&lt;img src=x onerror=alert(1)&gt;",
    +                          "name_href": "../././~/Foo.prototype.\"><img src=x onerror=alert(1)>.html",
    +                          "content": "<span>: <span>number</span></span>",
    +                          "anchor": {
    +                            "id": "property_&quot;&gt;&lt;img src=x onerror=alert(1)&gt;"
    +                          },
    +                          "tags": [],
    +                          "js_doc": null,
    +                          "source_href": null
    +                        },
                             {
                               "id": "property_foo",
                               "name": "foo",
    @@ -585,6 +597,164 @@
           ]
         }
       },
    +  {
    +    "html_head_ctx": {
    +      "title": "Foo.prototype.\"><img src=x onerror=alert(1)> - documentation",
    +      "current_file": ".",
    +      "stylesheet_url": "../styles.css",
    +      "page_stylesheet_url": "../page.css",
    +      "url_search_index": "../search_index.js",
    +      "script_js": "../script.js",
    +      "fuse_js": "../fuse.js",
    +      "url_search": "../search.js"
    +    },
    +    "sidepanel_ctx": {
    +      "package_name": null,
    +      "partitions": [
    +        {
    +          "name": "Class",
    +          "symbols": [
    +            {
    +              "kind": [
    +                {
    +                  "kind": "Class",
    +                  "char": "c",
    +                  "title": "Class",
    +                  "title_lowercase": "class",
    +                  "title_plural": "Classes"
    +                }
    +              ],
    +              "name": "Bar",
    +              "href": "../././~/Bar.html",
    +              "active": false,
    +              "deprecated": false
    +            },
    +            {
    +              "kind": [
    +                {
    +                  "kind": "Class",
    +                  "char": "c",
    +                  "title": "Class",
    +                  "title_lowercase": "class",
    +                  "title_plural": "Classes"
    +                }
    +              ],
    +              "name": "Foo",
    +              "href": "../././~/Foo.html",
    +              "active": false,
    +              "deprecated": false
    +            },
    +            {
    +              "kind": [
    +                {
    +                  "kind": "Class",
    +                  "char": "c",
    +                  "title": "Class",
    +                  "title_lowercase": "class",
    +                  "title_plural": "Classes"
    +                }
    +              ],
    +              "name": "Foobar",
    +              "href": "../././~/Foobar.html",
    +              "active": false,
    +              "deprecated": false
    +            }
    +          ]
    +        }
    +      ]
    +    },
    +    "symbol_group_ctx": {
    +      "name": "Foo.prototype.\"><img src=x onerror=alert(1)>",
    +      "symbols": [
    +        {
    +          "kind": {
    +            "kind": "Property",
    +            "char": " ",
    +            "title": "Property",
    +            "title_lowercase": "property",
    +            "title_plural": "Properties"
    +          },
    +          "tags": [],
    +          "subtitle": null,
    +          "content": [
    +            {
    +              "kind": "other",
    +              "value": {
    +                "id": "",
    +                "docs": null,
    +                "sections": [
    +                  {
    +                    "title": "Type",
    +                    "content": {
    +                      "kind": "doc_entry",
    +                      "content": [
    +                        {
    +                          "id": "variable_Foo_prototype_&quot;&gt;&lt;img src=x onerror=alert(1)&gt;",
    +                          "name": "",
    +                          "name_href": null,
    +                          "content": "<span>number</span>",
    +                          "anchor": {
    +                            "id": "variable_Foo_prototype_&quot;&gt;&lt;img src=x onerror=alert(1)&gt;"
    +                          },
    +                          "tags": [],
    +                          "js_doc": null,
    +                          "source_href": null
    +                        }
    +                      ]
    +                    }
    +                  }
    +                ]
    +              }
    +            }
    +          ],
    +          "deprecated": null,
    +          "source_href": null
    +        }
    +      ],
    +      "usages": {
    +        "show_tabs": false,
    +        "usages": [
    +          {
    +            "name": "",
    +            "content": "<div class=\"markdown flex-1\"><pre class=\"highlight\"><code><span style=\"font-weight:bold;color:#a71d5d;\">import </span><span style=\"color:#323232;\">{ Foo } </span><span style=\"font-weight:bold;color:#a71d5d;\">from </span><span style=\"color:#183691;\">\".\"</span><span style=\"color:#323232;\">;\n</span><span style=\"font-weight:bold;color:#a71d5d;\">const </span><span style=\"color:#323232;\">{ &amp;</span><span style=\"color:#0086b3;\">quot</span><span style=\"color:#323232;\">;&amp;</span><span style=\"color:#0086b3;\">gt</span><span style=\"color:#323232;\">;&amp;</span><span style=\"color:#0086b3;\">lt</span><span style=\"color:#323232;\">;</span><span style=\"color:#0086b3;\">img src</span><span style=\"font-weight:bold;color:#a71d5d;\">=</span><span style=\"color:#323232;\">x onerror</span><span style=\"font-weight:bold;color:#a71d5d;\">=</span><span style=\"font-weight:bold;color:#795da3;\">alert</span><span style=\"color:#323232;\">(</span><span style=\"color:#0086b3;\">1</span><span style=\"color:#323232;\">)</span><span style=\"font-weight:bold;color:#a71d5d;\">&amp;</span><span style=\"color:#323232;\">gt; } </span><span style=\"font-weight:bold;color:#a71d5d;\">= </span><span style=\"color:#0086b3;\">Foo</span><span style=\"color:#323232;\">.prototype;\n</span></code><button class=\"context_button\" data-copy=\"import { Foo } from &quot;.&quot;;\nconst { &amp;quot;&amp;gt;&amp;lt;img src=x onerror=alert(1)&amp;gt; } = Foo.prototype;\n\"><svg width=\"15\" height=\"15\" viewBox=\"0 0 15 15\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n  <path d=\"M1.55566 2.7C1.55566 2.03726 2.09292 1.5 2.75566 1.5H8.75566C9.41841 1.5 9.95566 2.03726 9.95566 2.7V5.1H12.3557C13.0184 5.1 13.5557 5.63726 13.5557 6.3V12.3C13.5557 12.9627 13.0184 13.5 12.3557 13.5H6.35566C5.69292 13.5 5.15566 12.9627 5.15566 12.3V9.9H2.75566C2.09292 9.9 1.55566 9.36274 1.55566 8.7V2.7ZM6.35566 9.9V12.3H12.3557V6.3H9.95566V8.7C9.95566 9.36274 9.41841 9.9 8.75566 9.9H6.35566ZM8.75566 8.7V2.7L2.75566 2.7V8.7H8.75566Z\" fill=\"#232323\"></path>\n</svg>\n</button><code></code></pre>\n</div>",
    +            "additional_css": ""
    +          }
    +        ]
    +      }
    +    },
    +    "breadcrumbs_ctx": {
    +      "parts": [
    +        {
    +          "name": "index",
    +          "href": "../",
    +          "is_symbol": false,
    +          "is_first_symbol": false,
    +          "is_all_symbols_part": false
    +        },
    +        {
    +          "name": "Foo",
    +          "href": "../././~/Foo.html",
    +          "is_symbol": true,
    +          "is_first_symbol": true,
    +          "is_all_symbols_part": false
    +        },
    +        {
    +          "name": "prototype",
    +          "href": "../././~/Foo.prototype.html",
    +          "is_symbol": true,
    +          "is_first_symbol": false,
    +          "is_all_symbols_part": false
    +        },
    +        {
    +          "name": "\"><img src=x onerror=alert(1)>",
    +          "href": "../././~/Foo.prototype.\"><img src=x onerror=alert(1)>.html",
    +          "is_symbol": true,
    +          "is_first_symbol": false,
    +          "is_all_symbols_part": false
    +        }
    +      ]
    +    }
    +  },
       {
         "html_head_ctx": {
           "title": "Foo.prototype.foo - documentation",
    
  • tests/testdata/symbol_group-tree-sitter.json+170 0 modified
    @@ -230,6 +230,18 @@
                         "content": {
                           "kind": "doc_entry",
                           "content": [
    +                        {
    +                          "id": "property_&quot;&gt;&lt;img src=x onerror=alert(1)&gt;",
    +                          "name": "&quot;&gt;&lt;img src=x onerror=alert(1)&gt;",
    +                          "name_href": "../././~/Foo.prototype.\"><img src=x onerror=alert(1)>.html",
    +                          "content": "<span>: <span>number</span></span>",
    +                          "anchor": {
    +                            "id": "property_&quot;&gt;&lt;img src=x onerror=alert(1)&gt;"
    +                          },
    +                          "tags": [],
    +                          "js_doc": null,
    +                          "source_href": null
    +                        },
                             {
                               "id": "property_foo",
                               "name": "foo",
    @@ -585,6 +597,164 @@
           ]
         }
       },
    +  {
    +    "html_head_ctx": {
    +      "title": "Foo.prototype.\"><img src=x onerror=alert(1)> - documentation",
    +      "current_file": ".",
    +      "stylesheet_url": "../styles.css",
    +      "page_stylesheet_url": "../page.css",
    +      "url_search_index": "../search_index.js",
    +      "script_js": "../script.js",
    +      "fuse_js": "../fuse.js",
    +      "url_search": "../search.js"
    +    },
    +    "sidepanel_ctx": {
    +      "package_name": null,
    +      "partitions": [
    +        {
    +          "name": "Class",
    +          "symbols": [
    +            {
    +              "kind": [
    +                {
    +                  "kind": "Class",
    +                  "char": "c",
    +                  "title": "Class",
    +                  "title_lowercase": "class",
    +                  "title_plural": "Classes"
    +                }
    +              ],
    +              "name": "Bar",
    +              "href": "../././~/Bar.html",
    +              "active": false,
    +              "deprecated": false
    +            },
    +            {
    +              "kind": [
    +                {
    +                  "kind": "Class",
    +                  "char": "c",
    +                  "title": "Class",
    +                  "title_lowercase": "class",
    +                  "title_plural": "Classes"
    +                }
    +              ],
    +              "name": "Foo",
    +              "href": "../././~/Foo.html",
    +              "active": false,
    +              "deprecated": false
    +            },
    +            {
    +              "kind": [
    +                {
    +                  "kind": "Class",
    +                  "char": "c",
    +                  "title": "Class",
    +                  "title_lowercase": "class",
    +                  "title_plural": "Classes"
    +                }
    +              ],
    +              "name": "Foobar",
    +              "href": "../././~/Foobar.html",
    +              "active": false,
    +              "deprecated": false
    +            }
    +          ]
    +        }
    +      ]
    +    },
    +    "symbol_group_ctx": {
    +      "name": "Foo.prototype.\"><img src=x onerror=alert(1)>",
    +      "symbols": [
    +        {
    +          "kind": {
    +            "kind": "Property",
    +            "char": " ",
    +            "title": "Property",
    +            "title_lowercase": "property",
    +            "title_plural": "Properties"
    +          },
    +          "tags": [],
    +          "subtitle": null,
    +          "content": [
    +            {
    +              "kind": "other",
    +              "value": {
    +                "id": "",
    +                "docs": null,
    +                "sections": [
    +                  {
    +                    "title": "Type",
    +                    "content": {
    +                      "kind": "doc_entry",
    +                      "content": [
    +                        {
    +                          "id": "variable_Foo_prototype_&quot;&gt;&lt;img src=x onerror=alert(1)&gt;",
    +                          "name": "",
    +                          "name_href": null,
    +                          "content": "<span>number</span>",
    +                          "anchor": {
    +                            "id": "variable_Foo_prototype_&quot;&gt;&lt;img src=x onerror=alert(1)&gt;"
    +                          },
    +                          "tags": [],
    +                          "js_doc": null,
    +                          "source_href": null
    +                        }
    +                      ]
    +                    }
    +                  }
    +                ]
    +              }
    +            }
    +          ],
    +          "deprecated": null,
    +          "source_href": null
    +        }
    +      ],
    +      "usages": {
    +        "show_tabs": false,
    +        "usages": [
    +          {
    +            "name": "",
    +            "content": "<div class=\"markdown flex-1\"><pre class=\"highlight\"><code><span class=\"pl-k\">import</span> { Foo } <span class=\"pl-k\">from</span> <span class=\"pl-s\">\".\"</span>;\n<span class=\"pl-k\">const</span> { <span class=\"pl-c1\">&amp;</span>quot;<span class=\"pl-c1\">&amp;</span>gt;<span class=\"pl-c1\">&amp;</span>lt;img src<span class=\"pl-c1\">=</span>x onerror<span class=\"pl-c1\">=</span>alert(<span class=\"pl-c1\">1</span>)<span class=\"pl-c1\">&amp;</span>gt; } <span class=\"pl-c1\">=</span> Foo.<span class=\"pl-c1\">prototype</span>;\n</code><button class=\"context_button\" data-copy=\"import { Foo } from &quot;.&quot;;\nconst { &amp;quot;&amp;gt;&amp;lt;img src=x onerror=alert(1)&amp;gt; } = Foo.prototype;\n\"><svg width=\"15\" height=\"15\" viewBox=\"0 0 15 15\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n  <path d=\"M1.55566 2.7C1.55566 2.03726 2.09292 1.5 2.75566 1.5H8.75566C9.41841 1.5 9.95566 2.03726 9.95566 2.7V5.1H12.3557C13.0184 5.1 13.5557 5.63726 13.5557 6.3V12.3C13.5557 12.9627 13.0184 13.5 12.3557 13.5H6.35566C5.69292 13.5 5.15566 12.9627 5.15566 12.3V9.9H2.75566C2.09292 9.9 1.55566 9.36274 1.55566 8.7V2.7ZM6.35566 9.9V12.3H12.3557V6.3H9.95566V8.7C9.95566 9.36274 9.41841 9.9 8.75566 9.9H6.35566ZM8.75566 8.7V2.7L2.75566 2.7V8.7H8.75566Z\" fill=\"#232323\"></path>\n</svg>\n</button><code></code></pre>\n</div>",
    +            "additional_css": ""
    +          }
    +        ]
    +      }
    +    },
    +    "breadcrumbs_ctx": {
    +      "parts": [
    +        {
    +          "name": "index",
    +          "href": "../",
    +          "is_symbol": false,
    +          "is_first_symbol": false,
    +          "is_all_symbols_part": false
    +        },
    +        {
    +          "name": "Foo",
    +          "href": "../././~/Foo.html",
    +          "is_symbol": true,
    +          "is_first_symbol": true,
    +          "is_all_symbols_part": false
    +        },
    +        {
    +          "name": "prototype",
    +          "href": "../././~/Foo.prototype.html",
    +          "is_symbol": true,
    +          "is_first_symbol": false,
    +          "is_all_symbols_part": false
    +        },
    +        {
    +          "name": "\"><img src=x onerror=alert(1)>",
    +          "href": "../././~/Foo.prototype.\"><img src=x onerror=alert(1)>.html",
    +          "is_symbol": true,
    +          "is_first_symbol": false,
    +          "is_all_symbols_part": false
    +        }
    +      ]
    +    }
    +  },
       {
         "html_head_ctx": {
           "title": "Foo.prototype.foo - documentation",
    
  • tests/testdata/symbol_search.json+4 4 modified
    @@ -23,9 +23,9 @@
           "file": ".",
           "location": {
             "filename": "",
    -        "line": 30,
    +        "line": 31,
             "col": 0,
    -        "byteIndex": 551
    +        "byteIndex": 591
           },
           "declarationKind": "export",
           "deprecated": false
    @@ -38,9 +38,9 @@
           "file": ".",
           "location": {
             "filename": "",
    -        "line": 33,
    +        "line": 34,
             "col": 0,
    -        "byteIndex": 585
    +        "byteIndex": 625
           },
           "declarationKind": "export",
           "deprecated": false
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.