GHSA-83RM-6C8R-G728

Vulnerability from github – Published: 2026-10-28 21:06 – Updated: 2026-10-28 21:06
VLAI
Summary
Vue I18n: escapeParameter does not prevent XSS through srcdoc in translation messages
Details

Summary

When escapeParameter is enabled (escapeParameterHtml in the Legacy API), Vue I18n passes the translated string through sanitizeTranslatedHtml(). The sanitizer escapes < and > in attribute values and neutralizes event handler attributes such as onerror, but it does not handle the srcdoc attribute of <iframe>.

The value of srcdoc is an HTML document: the browser decodes the character references in it and parses the result as a nested document in the same origin. The escaping and the neutralization done by the sanitizer are both character references, so they are undone inside srcdoc. This bypasses the fix for CVE-2025-53892 (GHSA-x8qp-wqqm-57ph).

Details

sanitizeTranslatedHtml() (packages/shared/src/sanitize.ts in v11.4.13) works on the final translated string:

  1. It escapes <, >, " and ' inside quoted attribute values.
  2. It rewrites the on of event handler attributes to &#111;n.
  3. It replaces javascript: URLs in href, src, action and formaction with about:blank.

Inside srcdoc, steps 1 and 2 only produce character references, which the browser decodes before parsing the value as a document. Markup inside srcdoc therefore survives the sanitizer, with escaping or without it.

This can be reached in three ways:

  • A parameter inside srcdoc. With a message such as <iframe srcdoc="{content}"></iframe>, escapeParameter escapes content into character references, and the browser decodes them again.
  • Markup in a message. A message that contains an <iframe srcdoc="..."> keeps it after sanitizing. The documentation asks not to use HTML messages with content from users, but it also says that the sanitizer neutralizes event handlers in the final HTML.
  • Together with GHSA-8rc2-428q-7ww3. When a call passes a list and named values, the named values are not escaped. An <iframe srcdoc="..."> in a named value then reaches the sanitizer as markup, even with an ordinary message such as {0} {name}.

PoC

import { createI18n } from 'vue-i18n'

const i18n = createI18n({
  legacy: false,
  locale: 'en',
  escapeParameter: true,
  messages: {
    en: { message: '<iframe srcdoc="<b>x</b>"></iframe>' }
  }
})

console.log(i18n.global.t('message'))
// v11.4.13: <iframe srcdoc="&lt;b&gt;x&lt;/b&gt;"></iframe>

The browser decodes the value and renders <b>x</b> as a separate document inside the <iframe>, in the same origin as the page. Any markup in that position is parsed the same way.

Impact

Applications are affected when they enable escapeParameter, render translations as HTML (for example with v-html), and one of the following applies:

  • an untrusted value is passed as a parameter that the message places inside srcdoc,
  • message content is not fully trusted (for example crowdsourced or stored translations), or
  • untrusted named values are passed in the same call as a list (GHSA-8rc2-428q-7ww3).

An attacker can then run script in the origin of the application (DOM-based XSS), despite the sanitizer.

Rendering through <i18n-t> is not affected, since it does not render translations as HTML.

Patches

Fixed in 11.4.14 and 12.0.0-alpha.5. The sanitizer now neutralizes the srcdoc attribute itself, in the same way as event handler attributes.

v9 and v10 are no longer supported and will not be fixed. Please migrate to v11.

Workarounds

  • Do not render translations that can contain untrusted content with v-html. Use component interpolation (<i18n-t>) instead.
  • Do not put srcdoc in translation messages, and do not pass values into it.

References

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "vue-i18n"
      },
      "versions": [
        "9.14.5"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "vue-i18n"
      },
      "versions": [
        "10.0.8"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "vue-i18n"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "11.1.10"
            },
            {
              "fixed": "11.4.14"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "vue-i18n"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "12.0.0-alpha.4"
            },
            {
              "fixed": "12.0.0-alpha.5"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "12.0.0-alpha.4"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "petite-vue-i18n"
      },
      "versions": [
        "9.14.5"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "petite-vue-i18n"
      },
      "versions": [
        "10.0.8"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "petite-vue-i18n"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "11.1.10"
            },
            {
              "fixed": "11.4.14"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "petite-vue-i18n"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "12.0.0-alpha.4"
            },
            {
              "fixed": "12.0.0-alpha.5"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "12.0.0-alpha.4"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/core-base"
      },
      "versions": [
        "9.14.5"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/core-base"
      },
      "versions": [
        "10.0.8"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/core-base"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "11.1.10"
            },
            {
              "fixed": "11.4.14"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/core-base"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "12.0.0-alpha.4"
            },
            {
              "fixed": "12.0.0-alpha.5"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "12.0.0-alpha.4"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/core"
      },
      "versions": [
        "9.14.5"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/core"
      },
      "versions": [
        "10.0.8"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/core"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "11.1.10"
            },
            {
              "fixed": "11.4.14"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/core"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "12.0.0-alpha.4"
            },
            {
              "fixed": "12.0.0-alpha.5"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "12.0.0-alpha.4"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/vue-i18n-core"
      },
      "versions": [
        "9.14.5"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/vue-i18n-core"
      },
      "versions": [
        "10.0.8"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/vue-i18n-core"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "11.1.10"
            },
            {
              "fixed": "11.4.14"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/vue-i18n-core"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "12.0.0-alpha.4"
            },
            {
              "fixed": "12.0.0-alpha.5"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "12.0.0-alpha.4"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/shared"
      },
      "versions": [
        "9.14.5"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/shared"
      },
      "versions": [
        "10.0.8"
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/shared"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "11.1.10"
            },
            {
              "fixed": "11.4.14"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@intlify/shared"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "12.0.0-alpha.4"
            },
            {
              "fixed": "12.0.0-alpha.5"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "12.0.0-alpha.4"
      ]
    }
  ],
  "aliases": [
    "CVE-2026-118257"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-116",
      "CWE-79"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-28T21:06:24Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "### Summary\n\nWhen `escapeParameter` is enabled (`escapeParameterHtml` in the Legacy API), Vue I18n passes the translated string through `sanitizeTranslatedHtml()`. The sanitizer escapes `\u003c` and `\u003e` in attribute values and neutralizes event handler attributes such as `onerror`, but it does not handle the `srcdoc` attribute of `\u003ciframe\u003e`.\n\nThe value of `srcdoc` is an HTML document: the browser decodes the character references in it and parses the result as a nested document in the same origin. The escaping and the neutralization done by the sanitizer are both character references, so they are undone inside `srcdoc`. This bypasses the fix for CVE-2025-53892 (GHSA-x8qp-wqqm-57ph).\n\n### Details\n\n`sanitizeTranslatedHtml()` ([`packages/shared/src/sanitize.ts` in v11.4.13](https://github.com/intlify/vue-i18n/blob/v11.4.13/packages/shared/src/sanitize.ts#L126-L162)) works on the final translated string:\n\n1. It escapes `\u003c`, `\u003e`, `\"` and `\u0027` inside quoted attribute values.\n2. It rewrites the `on` of event handler attributes to `\u0026#111;n`.\n3. It replaces `javascript:` URLs in `href`, `src`, `action` and `formaction` with `about:blank`.\n\nInside `srcdoc`, steps 1 and 2 only produce character references, which the browser decodes before parsing the value as a document. Markup inside `srcdoc` therefore survives the sanitizer, with escaping or without it.\n\nThis can be reached in three ways:\n\n- **A parameter inside `srcdoc`.** With a message such as `\u003ciframe srcdoc=\"{content}\"\u003e\u003c/iframe\u003e`, `escapeParameter` escapes `content` into character references, and the browser decodes them again.\n- **Markup in a message.** A message that contains an `\u003ciframe srcdoc=\"...\"\u003e` keeps it after sanitizing. The documentation asks not to use HTML messages with content from users, but it also says that the sanitizer neutralizes event handlers in the final HTML.\n- **Together with GHSA-8rc2-428q-7ww3.** When a call passes a list and named values, the named values are not escaped. An `\u003ciframe srcdoc=\"...\"\u003e` in a named value then reaches the sanitizer as markup, even with an ordinary message such as `{0} {name}`.\n\n### PoC\n\n```js\nimport { createI18n } from \u0027vue-i18n\u0027\n\nconst i18n = createI18n({\n  legacy: false,\n  locale: \u0027en\u0027,\n  escapeParameter: true,\n  messages: {\n    en: { message: \u0027\u003ciframe srcdoc=\"\u003cb\u003ex\u003c/b\u003e\"\u003e\u003c/iframe\u003e\u0027 }\n  }\n})\n\nconsole.log(i18n.global.t(\u0027message\u0027))\n// v11.4.13: \u003ciframe srcdoc=\"\u0026lt;b\u0026gt;x\u0026lt;/b\u0026gt;\"\u003e\u003c/iframe\u003e\n```\n\nThe browser decodes the value and renders `\u003cb\u003ex\u003c/b\u003e` as a separate document inside the `\u003ciframe\u003e`, in the same origin as the page. Any markup in that position is parsed the same way.\n\n### Impact\n\nApplications are affected when they enable `escapeParameter`, render translations as HTML (for example with `v-html`), and one of the following applies:\n\n- an untrusted value is passed as a parameter that the message places inside `srcdoc`,\n- message content is not fully trusted (for example crowdsourced or stored translations), or\n- untrusted named values are passed in the same call as a list (GHSA-8rc2-428q-7ww3).\n\nAn attacker can then run script in the origin of the application (DOM-based XSS), despite the sanitizer.\n\nRendering through `\u003ci18n-t\u003e` is not affected, since it does not render translations as HTML.\n\n### Patches\n\nFixed in 11.4.14 and 12.0.0-alpha.5. The sanitizer now neutralizes the `srcdoc` attribute itself, in the same way as event handler attributes.\n\nv9 and v10 are no longer supported and will not be fixed. Please migrate to v11.\n\n### Workarounds\n\n- Do not render translations that can contain untrusted content with `v-html`. Use component interpolation (`\u003ci18n-t\u003e`) instead.\n- Do not put `srcdoc` in translation messages, and do not pass values into it.\n\n### References\n\n- [CWE-79](https://cwe.mitre.org/data/definitions/79.html), [CWE-116](https://cwe.mitre.org/data/definitions/116.html)\n- [GHSA-x8qp-wqqm-57ph](https://github.com/intlify/vue-i18n/security/advisories/GHSA-x8qp-wqqm-57ph) (CVE-2025-53892): the fix that added `sanitizeTranslatedHtml()`\n- [GHSA-8rc2-428q-7ww3](https://github.com/intlify/vue-i18n/security/advisories/GHSA-8rc2-428q-7ww3): named values are not escaped when a list is also passed\n- [HTML Standard: the `srcdoc` attribute](https://html.spec.whatwg.org/multipage/iframe-embed-object.html#attr-iframe-srcdoc)",
  "id": "GHSA-83rm-6c8r-g728",
  "modified": "2026-10-28T21:06:24Z",
  "published": "2026-10-28T21:06:24Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/intlify/vue-i18n/security/advisories/GHSA-83rm-6c8r-g728"
    },
    {
      "type": "WEB",
      "url": "https://github.com/intlify/vue-i18n/pull/2679"
    },
    {
      "type": "WEB",
      "url": "https://github.com/intlify/vue-i18n/commit/43391d20e7c85403cb59bdb21b5d542819f54042"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/intlify/vue-i18n"
    },
    {
      "type": "WEB",
      "url": "https://github.com/intlify/vue-i18n/releases/tag/v11.4.14"
    },
    {
      "type": "WEB",
      "url": "https://github.com/intlify/vue-i18n/releases/tag/v12.0.0-alpha.5"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Vue I18n: escapeParameter does not prevent XSS through srcdoc in translation messages"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.