VYPR
Medium severityOSV Advisory· Published Jul 16, 2025· Updated Apr 15, 2026

CVE-2025-53892

CVE-2025-53892

Description

Vue I18n is the internationalization plugin for Vue.js. The escapeParameterHtml: true option in Vue I18n is designed to protect against HTML/script injection by escaping interpolated parameters. However, starting in version 9.0.0 and prior to versions 9.14.5, 10.0.8, and 11.1.0, this setting fails to prevent execution of certain tag-based payloads, such as <img src=x onerror=...>, if the interpolated value is inserted inside an HTML context using v-html. This may lead to a DOM-based XSS vulnerability, even when using escapeParameterHtml: true, if a translation string includes minor HTML and is rendered via v-html. Versions 9.14.5, 10.0.8, and 11.1.0 contain a fix for the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
vue-i18nnpm
>= 9.0.0, < 9.14.59.14.5
vue-i18nnpm
>= 10.0.0, < 10.0.810.0.8
vue-i18nnpm
>= 11.0.0, < 11.1.1011.1.10
@intlify/corenpm
>= 9.0.0, < 9.14.59.14.5
@intlify/corenpm
>= 10.0.0, < 10.0.810.0.8
@intlify/corenpm
>= 11.0.0, < 11.1.1011.1.10
@intlify/core-basenpm
>= 9.0.0, < 9.14.59.14.5
@intlify/core-basenpm
>= 10.0.0, < 10.0.810.0.8
@intlify/core-basenpm
>= 11.0.0, < 11.1.1011.1.10
@intlify/vue-i18n-corenpm
>= 9.2.0, < 9.14.59.14.5
@intlify/vue-i18n-corenpm
>= 10.0.0, < 10.0.810.0.8
@intlify/vue-i18n-corenpm
>= 11.0.0, < 11.1.1011.1.10
petite-vue-i18nnpm
>= 10.0.0, < 10.0.810.0.8
petite-vue-i18nnpm
>= 11.0.0, < 11.1.1011.1.10

Affected products

1

Patches

5
a47099619fb9

fix: DOM-based XSS via tag attributes for escape parameter (#2230)

https://github.com/intlify/vue-i18nkazuya kawaguchiJul 16, 2025via ghsa
6 files changed · +497 14
  • packages/core-base/src/translate.ts+17 2 modified
    @@ -9,6 +9,7 @@ import {
       generateFormatCacheKey,
       generateCodeFrame,
       escapeHtml,
    +  sanitizeTranslatedHtml,
       inBrowser,
       warn,
       mark,
    @@ -154,7 +155,16 @@ export interface TranslateOptions<Locales = Locale>
       fallbackWarn?: boolean
       /**
        * @remarks
    -   * Whether do escape parameter for list or named interpolation values
    +   * Whether to escape parameters for list or named interpolation values.
    +   * When enabled, this option:
    +   * - Escapes HTML special characters (`<`, `>`, `"`, `'`, `&`, `/`, `=`) in interpolation parameters
    +   * - Sanitizes the final translated HTML to prevent XSS attacks by:
    +   *   - Escaping dangerous characters in HTML attribute values
    +   *   - Neutralizing event handler attributes (onclick, onerror, etc.)
    +   *   - Disabling javascript: URLs in href, src, action, formaction, and style attributes
    +   *
    +   * @defaultValue false
    +   * @see [HTML Message - Using the escapeParameter option](https://vue-i18n.intlify.dev/guide/essentials/syntax.html#using-the-escapeparameter-option)
        */
       escapeParameter?: boolean
       /**
    @@ -763,10 +773,15 @@ export function translate<
       )
     
       // if use post translation option, proceed it with handler
    -  const ret = postTranslation
    +  let ret = postTranslation
         ? postTranslation(messaged, key as string)
         : messaged
     
    +  // apply HTML sanitization for security
    +  if (escapeParameter && isString(ret)) {
    +    ret = sanitizeTranslatedHtml(ret) as MessageFunctionReturn<Message>
    +  }
    +
       // NOTE: experimental !!
       if (__DEV__ || __FEATURE_PROD_INTLIFY_DEVTOOLS__) {
         // prettier-ignore
    
  • packages/core-base/test/translate.test.ts+68 2 modified
    @@ -679,7 +679,7 @@ describe('escapeParameter', () => {
         })
     
         expect(translate(ctx, 'hello', { name: '<b>kazupon</b>' })).toEqual(
    -      'hello, &lt;b&gt;kazupon&lt;/b&gt;!'
    +      'hello, &lt;b&gt;kazupon&lt;&#x2F;b&gt;!'
         )
       })
     
    @@ -697,7 +697,7 @@ describe('escapeParameter', () => {
     
         expect(
           translate(ctx, 'hello', ['<b>kazupon</b>'], { escapeParameter: true })
    -    ).toEqual('hello, &lt;b&gt;kazupon&lt;/b&gt;!')
    +    ).toEqual('hello, &lt;b&gt;kazupon&lt;&#x2F;b&gt;!')
       })
     
       test('no escape', () => {
    @@ -716,6 +716,72 @@ describe('escapeParameter', () => {
           'hello, <b>kazupon</b>!'
         )
       })
    +
    +  test('vulnerable case from GHSA report - img onerror attack', () => {
    +    // Mock console.warn to suppress warnings for this test
    +    const originalWarn = console.warn
    +    console.warn = vi.fn()
    +
    +    const ctx = context({
    +      locale: 'en',
    +      warnHtmlMessage: false,
    +      escapeParameter: true,
    +      messages: {
    +        en: {
    +          vulnerable: 'Caution: <img src=x onerror="{payload}">'
    +        }
    +      }
    +    })
    +
    +    const result = translate(ctx, 'vulnerable', {
    +      payload: '<script>alert("xss")</script>'
    +    })
    +
    +    // with the fix, the payload should be escaped, preventing the attack
    +    // The onerror attribute is neutralized by converting 'o' to &#111;
    +    expect(result).toEqual(
    +      'Caution: <img src=x &#111;nerror="&lt;script&gt;alert(&quot;xss&quot;)&lt;&#x2F;script&gt;">'
    +    )
    +
    +    // result should NOT contain executable script tags
    +    expect(result).not.toContain('<script>')
    +    expect(result).not.toContain('</script>')
    +
    +    // Restore console.warn
    +    console.warn = originalWarn
    +  })
    +
    +  test('vulnerable case - attribute injection attack', () => {
    +    const ctx = context({
    +      locale: 'en',
    +      warnHtmlMessage: false,
    +      escapeParameter: true,
    +      messages: {
    +        en: {
    +          message: 'Click <a href="{url}">here</a>'
    +        }
    +      }
    +    })
    +
    +    const result = translate(ctx, 'message', {
    +      url: 'javascript:alert(1)'
    +    })
    +
    +    // with the fix, javascript: URL scheme is neutralized
    +    expect(result).toEqual('Click <a href="javascript&#58;alert(1)">here</a>')
    +
    +    // another attack vector with quotes
    +    const result2 = translate(ctx, 'message', {
    +      url: '" onclick="alert(1)"'
    +    })
    +
    +    expect(result2).toEqual(
    +      'Click <a href="&quot; onclick&#x3D;&quot;alert(1)&quot;">here</a>'
    +    )
    +
    +    // `onclick` attribute should be escaped
    +    expect(result2).not.toContain('onclick=')
    +  })
     })
     
     describe('error', () => {
    
  • packages/shared/src/utils.ts+58 0 modified
    @@ -3,6 +3,8 @@
      * written by kazuya kawaguchi
      */
     
    +import { warn } from './warn'
    +
     export const inBrowser = typeof window !== 'undefined'
     
     export let mark: (tag: string) => void | undefined
    @@ -104,10 +106,66 @@ export const getGlobalThis = (): any => {
     
     export function escapeHtml(rawText: string): string {
       return rawText
    +    .replace(/&/g, '&amp;') // escape `&` first to avoid double escaping
         .replace(/</g, '&lt;')
         .replace(/>/g, '&gt;')
         .replace(/"/g, '&quot;')
         .replace(/'/g, '&apos;')
    +    .replace(/\//g, '&#x2F;') // escape `/` to prevent closing tags or JavaScript URLs
    +    .replace(/=/g, '&#x3D;') // escape `=` to prevent attribute injection
    +}
    +
    +function escapeAttributeValue(value: string): string {
    +  return value
    +    .replace(/&(?![a-zA-Z0-9#]{2,6};)/g, '&amp;') // escape unescaped `&`
    +    .replace(/"/g, '&quot;')
    +    .replace(/'/g, '&apos;')
    +    .replace(/</g, '&lt;')
    +    .replace(/>/g, '&gt;')
    +}
    +
    +export function sanitizeTranslatedHtml(html: string): string {
    +  // Escape dangerous characters in attribute values
    +  // Process attributes with double quotes
    +  html = html.replace(
    +    /(\w+)\s*=\s*"([^"]*)"/g,
    +    (_, attrName, attrValue) =>
    +      `${attrName}="${escapeAttributeValue(attrValue)}"`
    +  )
    +
    +  // Process attributes with single quotes
    +  html = html.replace(
    +    /(\w+)\s*=\s*'([^']*)'/g,
    +    (_, attrName, attrValue) =>
    +      `${attrName}='${escapeAttributeValue(attrValue)}'`
    +  )
    +
    +  // Detect and neutralize event handler attributes
    +  const eventHandlerPattern = /\s*on\w+\s*=\s*["']?[^"'>]+["']?/gi
    +  if (eventHandlerPattern.test(html)) {
    +    if (__DEV__) {
    +      warn(
    +        'Potentially dangerous event handlers detected in translation. ' +
    +          'Consider removing onclick, onerror, etc. from your translation messages.'
    +      )
    +    }
    +    // Neutralize event handler attributes by escaping 'on'
    +    html = html.replace(/(\s+)(on)(\w+\s*=)/gi, '$1&#111;n$3')
    +  }
    +
    +  // Disable javascript: URLs in various contexts
    +  const javascriptUrlPattern = [
    +    // In href, src, action, formaction attributes
    +    /(\s+(?:href|src|action|formaction)\s*=\s*["']?)\s*javascript:/gi,
    +    // In style attributes within url()
    +    /(style\s*=\s*["'][^"']*url\s*\(\s*)javascript:/gi
    +  ]
    +
    +  javascriptUrlPattern.forEach(pattern => {
    +    html = html.replace(pattern, '$1javascript&#58;')
    +  })
    +
    +  return html
     }
     
     const hasOwnProperty = Object.prototype.hasOwnProperty
    
  • packages/shared/test/utils.test.ts+338 2 modified
    @@ -1,9 +1,18 @@
    +import { vi } from 'vitest'
    +
    +// Mock the warn function before importing anything else
    +vi.mock('../src/warn', () => ({
    +  warn: vi.fn()
    +}))
    +
     import {
    +  escapeHtml,
       format,
       generateCodeFrame,
    -  makeSymbol,
       join,
    -  incrementer
    +  makeSymbol,
    +  incrementer,
    +  sanitizeTranslatedHtml
     } from '../src/index'
     
     test('format', () => {
    @@ -70,3 +79,330 @@ test('incrementer', () => {
       expect(inc2()).toBe(3)
       expect(inc2()).toBe(4)
     })
    +
    +describe('escapeHtml', () => {
    +  test('escape `<` and `>`', () => {
    +    expect(escapeHtml('<div>test</div>')).toBe(
    +      '&lt;div&gt;test&lt;&#x2F;div&gt;'
    +    )
    +  })
    +
    +  test('escape quotes', () => {
    +    expect(escapeHtml(`"double" and 'single'`)).toBe(
    +      '&quot;double&quot; and &apos;single&apos;'
    +    )
    +  })
    +
    +  test('escape `&` correctly', () => {
    +    expect(escapeHtml('&lt;')).toBe('&amp;lt;')
    +    expect(escapeHtml('&amp;')).toBe('&amp;amp;')
    +  })
    +
    +  test('escape `/` for preventing closing tags', () => {
    +    expect(escapeHtml('</script>')).toBe('&lt;&#x2F;script&gt;')
    +    expect(escapeHtml('javascript://')).toBe('javascript:&#x2F;&#x2F;')
    +  })
    +
    +  test('escape `=` for preventing attribute injection', () => {
    +    expect(escapeHtml('onerror=alert(1)')).toBe('onerror&#x3D;alert(1)')
    +    expect(escapeHtml('src=x onerror=alert(1)')).toBe(
    +      'src&#x3D;x onerror&#x3D;alert(1)'
    +    )
    +  })
    +
    +  test('prevent img `onerror` attack', () => {
    +    expect(escapeHtml('<img src=x onerror=alert(1)>')).toBe(
    +      '&lt;img src&#x3D;x onerror&#x3D;alert(1)&gt;'
    +    )
    +  })
    +
    +  test('prevent script injection', () => {
    +    expect(escapeHtml('</script><script>alert(1)</script>')).toBe(
    +      '&lt;&#x2F;script&gt;&lt;script&gt;alert(1)&lt;&#x2F;script&gt;'
    +    )
    +  })
    +
    +  test('handle complex XSS payloads', () => {
    +    expect(escapeHtml('<img src="x" onerror="alert(\'XSS\')">')).toBe(
    +      '&lt;img src&#x3D;&quot;x&quot; onerror&#x3D;&quot;alert(&apos;XSS&apos;)&quot;&gt;'
    +    )
    +  })
    +
    +  test('handle empty string', () => {
    +    expect(escapeHtml('')).toBe('')
    +  })
    +
    +  test('handle normal text without special characters', () => {
    +    expect(escapeHtml('Hello World')).toBe('Hello World')
    +  })
    +
    +  test('escape all special characters in order', () => {
    +    expect(escapeHtml('&<>"\'/=')).toBe('&amp;&lt;&gt;&quot;&apos;&#x2F;&#x3D;')
    +  })
    +})
    +
    +describe('sanitizeTranslatedHtml', () => {
    +  test('neutralize event handlers', () => {
    +    const html = '<a href="#" onclick="alert(1)">Click</a>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe('<a href="#" &#111;nclick="alert(1)">Click</a>')
    +  })
    +
    +  test('neutralize javascript URLs', () => {
    +    const html = '<a href="javascript:alert(1)">Click</a>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe('<a href="javascript&#58;alert(1)">Click</a>')
    +  })
    +
    +  test('escape dangerous characters in attribute values', () => {
    +    const html = '<div title="<script>alert(1)</script>">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div title="&lt;script&gt;alert(1)&lt;/script&gt;">Test</div>'
    +    )
    +  })
    +
    +  test('handle class attribute with normal values', () => {
    +    const html = '<div class="btn btn-primary">Button</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe('<div class="btn btn-primary">Button</div>')
    +  })
    +
    +  test('escape dangerous characters in class attribute', () => {
    +    const html = '<div class="normal&quot; onclick=&quot;alert(1)">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div class="normal&quot; &#111;nclick=&quot;alert(1)">Test</div>'
    +    )
    +  })
    +
    +  test('handle class attribute with script tags', () => {
    +    const html = '<div class="<script>alert(1)</script>">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div class="&lt;script&gt;alert(1)&lt;/script&gt;">Test</div>'
    +    )
    +  })
    +
    +  test('handle multiple attributes including class', () => {
    +    const html =
    +      '<div id="test" class="btn&quot; onclick=&quot;alert(1)" data-value="<>">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    // Check that dangerous characters in attribute values are escaped
    +    expect(result).toContain('class="btn&quot;')
    +    expect(result).toContain('data-value="&lt;&gt;"')
    +    // Check that onclick is neutralized
    +    expect(result).toContain('&#111;nclick=')
    +  })
    +
    +  test('handle class attribute with single quotes', () => {
    +    const html = "<div class='btn btn-danger'>Alert</div>"
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe("<div class='btn btn-danger'>Alert</div>")
    +  })
    +
    +  test('escape single quotes in class attribute with single quotes', () => {
    +    const html = "<div class='btn' onclick='alert(1)'>Test</div>"
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe("<div class='btn' &#111;nclick='alert(1)'>Test</div>")
    +  })
    +
    +  test('handle id attribute with dangerous characters', () => {
    +    const html = '<div id="test&quot; onclick=&quot;alert(1)">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div id="test&quot; &#111;nclick=&quot;alert(1)">Test</div>'
    +    )
    +  })
    +
    +  test('handle id attribute with script injection', () => {
    +    const html = '<div id="<script>alert(1)</script>">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div id="&lt;script&gt;alert(1)&lt;/script&gt;">Test</div>'
    +    )
    +  })
    +
    +  test('handle style attribute with dangerous characters', () => {
    +    const html =
    +      '<div style="color: red&quot; onclick=&quot;alert(1)">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div style="color: red&quot; &#111;nclick=&quot;alert(1)">Test</div>'
    +    )
    +  })
    +
    +  test('handle style attribute with javascript URL', () => {
    +    const html = '<div style="background: url(javascript:alert(1))">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div style="background: url(javascript&#58;alert(1))">Test</div>'
    +    )
    +  })
    +
    +  test('handle style attribute with javascript URL with spaces', () => {
    +    const html =
    +      '<div style="background: url( javascript:alert(1) )">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div style="background: url( javascript&#58;alert(1) )">Test</div>'
    +    )
    +  })
    +
    +  test('handle style attribute with uppercase JavaScript URL', () => {
    +    const html = '<div style="background: url(JAVASCRIPT:alert(1))">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div style="background: url(javascript&#58;alert(1))">Test</div>'
    +    )
    +  })
    +
    +  test('handle multiple dangerous attributes including id and style', () => {
    +    const html =
    +      '<div id="test&gt;" style="color: red" class="btn" onclick="alert(1)">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toContain('id="test&gt;"')
    +    expect(result).toContain('style="color: red"')
    +    expect(result).toContain('class="btn"')
    +    expect(result).toContain('&#111;nclick=')
    +  })
    +
    +  test('handle formaction attribute with javascript URL', () => {
    +    const html = '<button formaction="javascript:alert(1)">Submit</button>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<button formaction="javascript&#58;alert(1)">Submit</button>'
    +    )
    +  })
    +
    +  test('handle data attributes with javascript URL', () => {
    +    const html =
    +      '<div data-href="javascript:alert(1)" data-value="safe">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    // data-* attributes are not sanitized as they don't execute directly
    +    expect(result).toBe(
    +      '<div data-href="javascript:alert(1)" data-value="safe">Test</div>'
    +    )
    +  })
    +
    +  test('handle srcdoc attribute', () => {
    +    const html = '<iframe srcdoc="<script>alert(1)</script>">Test</iframe>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<iframe srcdoc="&lt;script&gt;alert(1)&lt;/script&gt;">Test</iframe>'
    +    )
    +  })
    +
    +  test('handle attribute values without quotes', () => {
    +    const html = '<img src=x onerror=alert(1)>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe('<img src=x &#111;nerror=alert(1)>')
    +  })
    +
    +  test('handle mixed quote styles', () => {
    +    const html = `<div title='test" onclick="alert(1)'>Test</div>`
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      `<div title='test&quot; &#111;nclick=&quot;alert(1)'>Test</div>`
    +    )
    +  })
    +
    +  test('handle HTML entities in attribute values', () => {
    +    const html = '<div title="&lt;script&gt;alert(1)&lt;/script&gt;">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    // Already escaped entities should remain as is
    +    expect(result).toBe(
    +      '<div title="&lt;script&gt;alert(1)&lt;/script&gt;">Test</div>'
    +    )
    +  })
    +
    +  test('handle nested quotes in attributes', () => {
    +    const html = `<div title='"hello" onclick="alert(1)"'>Test</div>`
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      `<div title='&quot;hello&quot; &#111;nclick=&quot;alert(1)&quot;'>Test</div>`
    +    )
    +  })
    +
    +  // Accessibility (a11y) attributes tests
    +  test('handle aria-label with dangerous characters', () => {
    +    const html =
    +      '<button aria-label="Click to <script>alert(1)</script>">Button</button>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<button aria-label="Click to &lt;script&gt;alert(1)&lt;/script&gt;">Button</button>'
    +    )
    +  })
    +
    +  test('handle aria-describedby with quotes', () => {
    +    const html = `<input aria-describedby="desc&quot; onclick=&quot;alert(1)" />`
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<input aria-describedby="desc&quot; &#111;nclick=&quot;alert(1)" />'
    +    )
    +  })
    +
    +  test('handle role attribute with dangerous characters', () => {
    +    const html = '<div role="button&quot; onclick=&quot;alert(1)">Click</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div role="button&quot; &#111;nclick=&quot;alert(1)">Click</div>'
    +    )
    +  })
    +
    +  test('handle tabindex attribute', () => {
    +    const html =
    +      '<div tabindex="0&quot; onclick=&quot;alert(1)">Focusable</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div tabindex="0&quot; &#111;nclick=&quot;alert(1)">Focusable</div>'
    +    )
    +  })
    +
    +  test('handle alt attribute with dangerous content', () => {
    +    const html =
    +      '<img src="test.jpg" alt="Image of <script>alert(1)</script>" />'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<img src="test.jpg" alt="Image of &lt;script&gt;alert(1)&lt;/script&gt;" />'
    +    )
    +  })
    +
    +  test('handle title attribute with dangerous content', () => {
    +    const html = '<abbr title="<img src=x onerror=alert(1)>">XSS</abbr>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<abbr title="&lt;img src=x &#111;nerror=alert(1)&gt;">XSS</abbr>'
    +    )
    +  })
    +
    +  test('handle multiple a11y attributes with dangerous content', () => {
    +    const html =
    +      '<button aria-label="<script>alert(1)</script>" role="button&quot; onclick=&quot;alert(1)" tabindex="0">Click</button>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toContain(
    +      'aria-label="&lt;script&gt;alert(1)&lt;/script&gt;"'
    +    )
    +    expect(result).toContain('role="button&quot;')
    +    expect(result).toContain('&#111;nclick=')
    +    expect(result).toContain('tabindex="0"')
    +  })
    +
    +  test('handle aria-hidden attribute', () => {
    +    const html =
    +      '<div aria-hidden="true&quot; onclick=&quot;alert(1)">Hidden</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div aria-hidden="true&quot; &#111;nclick=&quot;alert(1)">Hidden</div>'
    +    )
    +  })
    +
    +  test('handle aria-live attribute', () => {
    +    const html =
    +      '<div aria-live="polite" aria-label="Status: <b>Active</b>">Status</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div aria-live="polite" aria-label="Status: &lt;b&gt;Active&lt;/b&gt;">Status</div>'
    +    )
    +  })
    +})
    
  • packages/vue-i18n-core/src/composer.ts+8 4 modified
    @@ -476,17 +476,21 @@ export interface ComposerOptions<
       warnHtmlMessage?: boolean
       /**
        * @remarks
    -   * If `escapeParameter` is configured as true then interpolation parameters are escaped before the message is translated.
    +   * Whether to escape parameters for list or named interpolation values.
    +   * When enabled, this option:
    +   * - Escapes HTML special characters (`<`, `>`, `"`, `'`, `&`, `/`, `=`) in interpolation parameters
    +   * - Sanitizes the final translated HTML to prevent XSS attacks by:
    +   *   - Escaping dangerous characters in HTML attribute values
    +   *   - Neutralizing event handler attributes (onclick, onerror, etc.)
    +   *   - Disabling javascript: URLs in href, src, action, formaction, and style attributes
        *
        * This is useful when translation output is used in `v-html` and the translation resource contains html markup (e.g. <b> around a user provided value).
        *
        * This usage pattern mostly occurs when passing precomputed text strings into UI components.
        *
    -   * The escape process involves replacing the following symbols with their respective HTML character entities: `<`, `>`, `"`, `'`.
    -   *
        * Setting `escapeParameter` as true should not break existing functionality but provides a safeguard against a subtle type of XSS attack vectors.
        *
    -   * @VueI18nSee [HTML Message](../guide/essentials/syntax#html-message)
    +   * @VueI18nSee [HTML Message - Using the escapeParameter option](../guide/essentials/syntax#using-the-escapeparameter-option)
        *
        * @defaultValue `false`
        */
    
  • packages/vue-i18n-core/src/legacy.ts+8 4 modified
    @@ -270,17 +270,21 @@ export interface VueI18nOptions<
       warnHtmlInMessage?: WarnHtmlInMessageLevel
       /**
        * @remarks
    -   * If `escapeParameterHtml` is configured as true then interpolation parameters are escaped before the message is translated.
    +   * Whether to escape parameters for list or named interpolation values.
    +   * When enabled, this option:
    +   * - Escapes HTML special characters (`<`, `>`, `"`, `'`, `&`, `/`, `=`) in interpolation parameters
    +   * - Sanitizes the final translated HTML to prevent XSS attacks by:
    +   *   - Escaping dangerous characters in HTML attribute values
    +   *   - Neutralizing event handler attributes (onclick, onerror, etc.)
    +   *   - Disabling javascript: URLs in href, src, action, formaction, and style attributes
        *
        * This is useful when translation output is used in `v-html` and the translation resource contains html markup (e.g. <b> around a user provided value).
        *
        * This usage pattern mostly occurs when passing precomputed text strings into UI components.
        *
    -   * The escape process involves replacing the following symbols with their respective HTML character entities: `<`, `>`, `"`, `'`.
    -   *
        * Setting `escapeParameterHtml` as true should not break existing functionality but provides a safeguard against a subtle type of XSS attack vectors.
        *
    -   * @VueI18nSee [HTML Message](../guide/essentials/syntax#html-message)
    +   * @VueI18nSee [HTML Message - Using the escapeParameter option](../guide/essentials/syntax#using-the-escapeparameter-option)
        *
        * @defaultValue `false`
        */
    
49f982443ab8

fix: escapeParameterHtml does not prevent DOM-based XSS via tag attributes like onerror (#2229)

https://github.com/intlify/vue-i18nkazuya kawaguchiJul 16, 2025via ghsa
6 files changed · +501 13
  • packages/core-base/src/translate.ts+17 2 modified
    @@ -15,6 +15,7 @@ import {
       isString,
       mark,
       measure,
    +  sanitizeTranslatedHtml,
       warn
     } from '@intlify/shared'
     import { isMessageAST } from './ast'
    @@ -153,7 +154,16 @@ export interface TranslateOptions<Locales = Locale>
       fallbackWarn?: boolean
       /**
        * @remarks
    -   * Whether do escape parameter for list or named interpolation values
    +   * Whether to escape parameters for list or named interpolation values.
    +   * When enabled, this option:
    +   * - Escapes HTML special characters (`<`, `>`, `"`, `'`, `&`, `/`, `=`) in interpolation parameters
    +   * - Sanitizes the final translated HTML to prevent XSS attacks by:
    +   *   - Escaping dangerous characters in HTML attribute values
    +   *   - Neutralizing event handler attributes (onclick, onerror, etc.)
    +   *   - Disabling javascript: URLs in href, src, action, formaction, and style attributes
    +   *
    +   * @defaultValue false
    +   * @see [HTML Message - Using the escapeParameter option](https://vue-i18n.intlify.dev/guide/essentials/syntax.html#using-the-escapeparameter-option)
        */
       escapeParameter?: boolean
       /**
    @@ -765,10 +775,15 @@ export function translate<
       )
     
       // if use post translation option, proceed it with handler
    -  const ret = postTranslation
    +  let ret = postTranslation
         ? postTranslation(messaged, key as string)
         : messaged
     
    +  // apply HTML sanitization for security
    +  if (escapeParameter && isString(ret)) {
    +    ret = sanitizeTranslatedHtml(ret) as MessageFunctionReturn<Message>
    +  }
    +
       // NOTE: experimental !!
       if (__DEV__ || __FEATURE_PROD_INTLIFY_DEVTOOLS__) {
         // prettier-ignore
    
  • packages/core-base/test/translate.test.ts+68 2 modified
    @@ -685,7 +685,7 @@ describe('escapeParameter', () => {
         })
     
         expect(translate(ctx, 'hello', { name: '<b>kazupon</b>' })).toEqual(
    -      'hello, &lt;b&gt;kazupon&lt;/b&gt;!'
    +      'hello, &lt;b&gt;kazupon&lt;&#x2F;b&gt;!'
         )
       })
     
    @@ -703,7 +703,7 @@ describe('escapeParameter', () => {
     
         expect(
           translate(ctx, 'hello', ['<b>kazupon</b>'], { escapeParameter: true })
    -    ).toEqual('hello, &lt;b&gt;kazupon&lt;/b&gt;!')
    +    ).toEqual('hello, &lt;b&gt;kazupon&lt;&#x2F;b&gt;!')
       })
     
       test('no escape', () => {
    @@ -722,6 +722,72 @@ describe('escapeParameter', () => {
           'hello, <b>kazupon</b>!'
         )
       })
    +
    +  test('vulnerable case from GHSA report - img onerror attack', () => {
    +    // Mock console.warn to suppress warnings for this test
    +    const originalWarn = console.warn
    +    console.warn = vi.fn()
    +
    +    const ctx = context({
    +      locale: 'en',
    +      warnHtmlMessage: false,
    +      escapeParameter: true,
    +      messages: {
    +        en: {
    +          vulnerable: 'Caution: <img src=x onerror="{payload}">'
    +        }
    +      }
    +    })
    +
    +    const result = translate(ctx, 'vulnerable', {
    +      payload: '<script>alert("xss")</script>'
    +    })
    +
    +    // with the fix, the payload should be escaped, preventing the attack
    +    // The onerror attribute is neutralized by converting 'o' to &#111;
    +    expect(result).toEqual(
    +      'Caution: <img src=x &#111;nerror="&lt;script&gt;alert(&quot;xss&quot;)&lt;&#x2F;script&gt;">'
    +    )
    +
    +    // result should NOT contain executable script tags
    +    expect(result).not.toContain('<script>')
    +    expect(result).not.toContain('</script>')
    +
    +    // Restore console.warn
    +    console.warn = originalWarn
    +  })
    +
    +  test('vulnerable case - attribute injection attack', () => {
    +    const ctx = context({
    +      locale: 'en',
    +      warnHtmlMessage: false,
    +      escapeParameter: true,
    +      messages: {
    +        en: {
    +          message: 'Click <a href="{url}">here</a>'
    +        }
    +      }
    +    })
    +
    +    const result = translate(ctx, 'message', {
    +      url: 'javascript:alert(1)'
    +    })
    +
    +    // with the fix, javascript: URL scheme is neutralized
    +    expect(result).toEqual('Click <a href="javascript&#58;alert(1)">here</a>')
    +
    +    // another attack vector with quotes
    +    const result2 = translate(ctx, 'message', {
    +      url: '" onclick="alert(1)"'
    +    })
    +
    +    expect(result2).toEqual(
    +      'Click <a href="&quot; onclick&#x3D;&quot;alert(1)&quot;">here</a>'
    +    )
    +
    +    // `onclick` attribute should be escaped
    +    expect(result2).not.toContain('onclick=')
    +  })
     })
     
     describe('error', () => {
    
  • packages/shared/src/utils.ts+58 0 modified
    @@ -3,6 +3,8 @@
      * written by kazuya kawaguchi
      */
     
    +import { warn } from './warn'
    +
     export const inBrowser = typeof window !== 'undefined'
     
     export let mark: (tag: string) => void | undefined
    @@ -104,10 +106,66 @@ export const getGlobalThis = (): any => {
     
     export function escapeHtml(rawText: string): string {
       return rawText
    +    .replace(/&/g, '&amp;') // escape `&` first to avoid double escaping
         .replace(/</g, '&lt;')
         .replace(/>/g, '&gt;')
         .replace(/"/g, '&quot;')
         .replace(/'/g, '&apos;')
    +    .replace(/\//g, '&#x2F;') // escape `/` to prevent closing tags or JavaScript URLs
    +    .replace(/=/g, '&#x3D;') // escape `=` to prevent attribute injection
    +}
    +
    +function escapeAttributeValue(value: string): string {
    +  return value
    +    .replace(/&(?![a-zA-Z0-9#]{2,6};)/g, '&amp;') // escape unescaped `&`
    +    .replace(/"/g, '&quot;')
    +    .replace(/'/g, '&apos;')
    +    .replace(/</g, '&lt;')
    +    .replace(/>/g, '&gt;')
    +}
    +
    +export function sanitizeTranslatedHtml(html: string): string {
    +  // Escape dangerous characters in attribute values
    +  // Process attributes with double quotes
    +  html = html.replace(
    +    /(\w+)\s*=\s*"([^"]*)"/g,
    +    (_, attrName, attrValue) =>
    +      `${attrName}="${escapeAttributeValue(attrValue)}"`
    +  )
    +
    +  // Process attributes with single quotes
    +  html = html.replace(
    +    /(\w+)\s*=\s*'([^']*)'/g,
    +    (_, attrName, attrValue) =>
    +      `${attrName}='${escapeAttributeValue(attrValue)}'`
    +  )
    +
    +  // Detect and neutralize event handler attributes
    +  const eventHandlerPattern = /\s*on\w+\s*=\s*["']?[^"'>]+["']?/gi
    +  if (eventHandlerPattern.test(html)) {
    +    if (__DEV__) {
    +      warn(
    +        'Potentially dangerous event handlers detected in translation. ' +
    +          'Consider removing onclick, onerror, etc. from your translation messages.'
    +      )
    +    }
    +    // Neutralize event handler attributes by escaping 'on'
    +    html = html.replace(/(\s+)(on)(\w+\s*=)/gi, '$1&#111;n$3')
    +  }
    +
    +  // Disable javascript: URLs in various contexts
    +  const javascriptUrlPattern = [
    +    // In href, src, action, formaction attributes
    +    /(\s+(?:href|src|action|formaction)\s*=\s*["']?)\s*javascript:/gi,
    +    // In style attributes within url()
    +    /(style\s*=\s*["'][^"']*url\s*\(\s*)javascript:/gi
    +  ]
    +
    +  javascriptUrlPattern.forEach(pattern => {
    +    html = html.replace(pattern, '$1javascript&#58;')
    +  })
    +
    +  return html
     }
     
     const hasOwnProperty = Object.prototype.hasOwnProperty
    
  • packages/shared/test/utils.test.ts+342 1 modified
    @@ -1,4 +1,18 @@
    -import { format, generateCodeFrame, makeSymbol, join } from '../src/index'
    +import { vi } from 'vitest'
    +
    +// Mock the warn function before importing anything else
    +vi.mock('../src/warn', () => ({
    +  warn: vi.fn()
    +}))
    +
    +import {
    +  escapeHtml,
    +  format,
    +  generateCodeFrame,
    +  join,
    +  makeSymbol,
    +  sanitizeTranslatedHtml
    +} from '../src/index'
     
     test('format', () => {
       expect(format(`foo: {0}`, 'x')).toEqual('foo: x')
    @@ -54,3 +68,330 @@ test('join', () => {
       ]
       expect(join(longSize, ' ')).toEqual(longSize.join(' '))
     })
    +
    +describe('escapeHtml', () => {
    +  test('escape `<` and `>`', () => {
    +    expect(escapeHtml('<div>test</div>')).toBe(
    +      '&lt;div&gt;test&lt;&#x2F;div&gt;'
    +    )
    +  })
    +
    +  test('escape quotes', () => {
    +    expect(escapeHtml(`"double" and 'single'`)).toBe(
    +      '&quot;double&quot; and &apos;single&apos;'
    +    )
    +  })
    +
    +  test('escape `&` correctly', () => {
    +    expect(escapeHtml('&lt;')).toBe('&amp;lt;')
    +    expect(escapeHtml('&amp;')).toBe('&amp;amp;')
    +  })
    +
    +  test('escape `/` for preventing closing tags', () => {
    +    expect(escapeHtml('</script>')).toBe('&lt;&#x2F;script&gt;')
    +    expect(escapeHtml('javascript://')).toBe('javascript:&#x2F;&#x2F;')
    +  })
    +
    +  test('escape `=` for preventing attribute injection', () => {
    +    expect(escapeHtml('onerror=alert(1)')).toBe('onerror&#x3D;alert(1)')
    +    expect(escapeHtml('src=x onerror=alert(1)')).toBe(
    +      'src&#x3D;x onerror&#x3D;alert(1)'
    +    )
    +  })
    +
    +  test('prevent img `onerror` attack', () => {
    +    expect(escapeHtml('<img src=x onerror=alert(1)>')).toBe(
    +      '&lt;img src&#x3D;x onerror&#x3D;alert(1)&gt;'
    +    )
    +  })
    +
    +  test('prevent script injection', () => {
    +    expect(escapeHtml('</script><script>alert(1)</script>')).toBe(
    +      '&lt;&#x2F;script&gt;&lt;script&gt;alert(1)&lt;&#x2F;script&gt;'
    +    )
    +  })
    +
    +  test('handle complex XSS payloads', () => {
    +    expect(escapeHtml('<img src="x" onerror="alert(\'XSS\')">')).toBe(
    +      '&lt;img src&#x3D;&quot;x&quot; onerror&#x3D;&quot;alert(&apos;XSS&apos;)&quot;&gt;'
    +    )
    +  })
    +
    +  test('handle empty string', () => {
    +    expect(escapeHtml('')).toBe('')
    +  })
    +
    +  test('handle normal text without special characters', () => {
    +    expect(escapeHtml('Hello World')).toBe('Hello World')
    +  })
    +
    +  test('escape all special characters in order', () => {
    +    expect(escapeHtml('&<>"\'/=')).toBe('&amp;&lt;&gt;&quot;&apos;&#x2F;&#x3D;')
    +  })
    +})
    +
    +describe('sanitizeTranslatedHtml', () => {
    +  test('neutralize event handlers', () => {
    +    const html = '<a href="#" onclick="alert(1)">Click</a>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe('<a href="#" &#111;nclick="alert(1)">Click</a>')
    +  })
    +
    +  test('neutralize javascript URLs', () => {
    +    const html = '<a href="javascript:alert(1)">Click</a>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe('<a href="javascript&#58;alert(1)">Click</a>')
    +  })
    +
    +  test('escape dangerous characters in attribute values', () => {
    +    const html = '<div title="<script>alert(1)</script>">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div title="&lt;script&gt;alert(1)&lt;/script&gt;">Test</div>'
    +    )
    +  })
    +
    +  test('handle class attribute with normal values', () => {
    +    const html = '<div class="btn btn-primary">Button</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe('<div class="btn btn-primary">Button</div>')
    +  })
    +
    +  test('escape dangerous characters in class attribute', () => {
    +    const html = '<div class="normal&quot; onclick=&quot;alert(1)">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div class="normal&quot; &#111;nclick=&quot;alert(1)">Test</div>'
    +    )
    +  })
    +
    +  test('handle class attribute with script tags', () => {
    +    const html = '<div class="<script>alert(1)</script>">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div class="&lt;script&gt;alert(1)&lt;/script&gt;">Test</div>'
    +    )
    +  })
    +
    +  test('handle multiple attributes including class', () => {
    +    const html =
    +      '<div id="test" class="btn&quot; onclick=&quot;alert(1)" data-value="<>">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    // Check that dangerous characters in attribute values are escaped
    +    expect(result).toContain('class="btn&quot;')
    +    expect(result).toContain('data-value="&lt;&gt;"')
    +    // Check that onclick is neutralized
    +    expect(result).toContain('&#111;nclick=')
    +  })
    +
    +  test('handle class attribute with single quotes', () => {
    +    const html = "<div class='btn btn-danger'>Alert</div>"
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe("<div class='btn btn-danger'>Alert</div>")
    +  })
    +
    +  test('escape single quotes in class attribute with single quotes', () => {
    +    const html = "<div class='btn' onclick='alert(1)'>Test</div>"
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe("<div class='btn' &#111;nclick='alert(1)'>Test</div>")
    +  })
    +
    +  test('handle id attribute with dangerous characters', () => {
    +    const html = '<div id="test&quot; onclick=&quot;alert(1)">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div id="test&quot; &#111;nclick=&quot;alert(1)">Test</div>'
    +    )
    +  })
    +
    +  test('handle id attribute with script injection', () => {
    +    const html = '<div id="<script>alert(1)</script>">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div id="&lt;script&gt;alert(1)&lt;/script&gt;">Test</div>'
    +    )
    +  })
    +
    +  test('handle style attribute with dangerous characters', () => {
    +    const html =
    +      '<div style="color: red&quot; onclick=&quot;alert(1)">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div style="color: red&quot; &#111;nclick=&quot;alert(1)">Test</div>'
    +    )
    +  })
    +
    +  test('handle style attribute with javascript URL', () => {
    +    const html = '<div style="background: url(javascript:alert(1))">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div style="background: url(javascript&#58;alert(1))">Test</div>'
    +    )
    +  })
    +
    +  test('handle style attribute with javascript URL with spaces', () => {
    +    const html =
    +      '<div style="background: url( javascript:alert(1) )">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div style="background: url( javascript&#58;alert(1) )">Test</div>'
    +    )
    +  })
    +
    +  test('handle style attribute with uppercase JavaScript URL', () => {
    +    const html = '<div style="background: url(JAVASCRIPT:alert(1))">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div style="background: url(javascript&#58;alert(1))">Test</div>'
    +    )
    +  })
    +
    +  test('handle multiple dangerous attributes including id and style', () => {
    +    const html =
    +      '<div id="test&gt;" style="color: red" class="btn" onclick="alert(1)">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toContain('id="test&gt;"')
    +    expect(result).toContain('style="color: red"')
    +    expect(result).toContain('class="btn"')
    +    expect(result).toContain('&#111;nclick=')
    +  })
    +
    +  test('handle formaction attribute with javascript URL', () => {
    +    const html = '<button formaction="javascript:alert(1)">Submit</button>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<button formaction="javascript&#58;alert(1)">Submit</button>'
    +    )
    +  })
    +
    +  test('handle data attributes with javascript URL', () => {
    +    const html =
    +      '<div data-href="javascript:alert(1)" data-value="safe">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    // data-* attributes are not sanitized as they don't execute directly
    +    expect(result).toBe(
    +      '<div data-href="javascript:alert(1)" data-value="safe">Test</div>'
    +    )
    +  })
    +
    +  test('handle srcdoc attribute', () => {
    +    const html = '<iframe srcdoc="<script>alert(1)</script>">Test</iframe>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<iframe srcdoc="&lt;script&gt;alert(1)&lt;/script&gt;">Test</iframe>'
    +    )
    +  })
    +
    +  test('handle attribute values without quotes', () => {
    +    const html = '<img src=x onerror=alert(1)>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe('<img src=x &#111;nerror=alert(1)>')
    +  })
    +
    +  test('handle mixed quote styles', () => {
    +    const html = `<div title='test" onclick="alert(1)'>Test</div>`
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      `<div title='test&quot; &#111;nclick=&quot;alert(1)'>Test</div>`
    +    )
    +  })
    +
    +  test('handle HTML entities in attribute values', () => {
    +    const html = '<div title="&lt;script&gt;alert(1)&lt;/script&gt;">Test</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    // Already escaped entities should remain as is
    +    expect(result).toBe(
    +      '<div title="&lt;script&gt;alert(1)&lt;/script&gt;">Test</div>'
    +    )
    +  })
    +
    +  test('handle nested quotes in attributes', () => {
    +    const html = `<div title='"hello" onclick="alert(1)"'>Test</div>`
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      `<div title='&quot;hello&quot; &#111;nclick=&quot;alert(1)&quot;'>Test</div>`
    +    )
    +  })
    +
    +  // Accessibility (a11y) attributes tests
    +  test('handle aria-label with dangerous characters', () => {
    +    const html =
    +      '<button aria-label="Click to <script>alert(1)</script>">Button</button>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<button aria-label="Click to &lt;script&gt;alert(1)&lt;/script&gt;">Button</button>'
    +    )
    +  })
    +
    +  test('handle aria-describedby with quotes', () => {
    +    const html = `<input aria-describedby="desc&quot; onclick=&quot;alert(1)" />`
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<input aria-describedby="desc&quot; &#111;nclick=&quot;alert(1)" />'
    +    )
    +  })
    +
    +  test('handle role attribute with dangerous characters', () => {
    +    const html = '<div role="button&quot; onclick=&quot;alert(1)">Click</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div role="button&quot; &#111;nclick=&quot;alert(1)">Click</div>'
    +    )
    +  })
    +
    +  test('handle tabindex attribute', () => {
    +    const html =
    +      '<div tabindex="0&quot; onclick=&quot;alert(1)">Focusable</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div tabindex="0&quot; &#111;nclick=&quot;alert(1)">Focusable</div>'
    +    )
    +  })
    +
    +  test('handle alt attribute with dangerous content', () => {
    +    const html =
    +      '<img src="test.jpg" alt="Image of <script>alert(1)</script>" />'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<img src="test.jpg" alt="Image of &lt;script&gt;alert(1)&lt;/script&gt;" />'
    +    )
    +  })
    +
    +  test('handle title attribute with dangerous content', () => {
    +    const html = '<abbr title="<img src=x onerror=alert(1)>">XSS</abbr>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<abbr title="&lt;img src=x &#111;nerror=alert(1)&gt;">XSS</abbr>'
    +    )
    +  })
    +
    +  test('handle multiple a11y attributes with dangerous content', () => {
    +    const html =
    +      '<button aria-label="<script>alert(1)</script>" role="button&quot; onclick=&quot;alert(1)" tabindex="0">Click</button>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toContain(
    +      'aria-label="&lt;script&gt;alert(1)&lt;/script&gt;"'
    +    )
    +    expect(result).toContain('role="button&quot;')
    +    expect(result).toContain('&#111;nclick=')
    +    expect(result).toContain('tabindex="0"')
    +  })
    +
    +  test('handle aria-hidden attribute', () => {
    +    const html =
    +      '<div aria-hidden="true&quot; onclick=&quot;alert(1)">Hidden</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div aria-hidden="true&quot; &#111;nclick=&quot;alert(1)">Hidden</div>'
    +    )
    +  })
    +
    +  test('handle aria-live attribute', () => {
    +    const html =
    +      '<div aria-live="polite" aria-label="Status: <b>Active</b>">Status</div>'
    +    const result = sanitizeTranslatedHtml(html)
    +    expect(result).toBe(
    +      '<div aria-live="polite" aria-label="Status: &lt;b&gt;Active&lt;/b&gt;">Status</div>'
    +    )
    +  })
    +})
    
  • packages/vue-i18n-core/src/composer.ts+8 4 modified
    @@ -473,17 +473,21 @@ export interface ComposerOptions<
       warnHtmlMessage?: boolean
       /**
        * @remarks
    -   * If `escapeParameter` is configured as true then interpolation parameters are escaped before the message is translated.
    +   * Whether to escape parameters for list or named interpolation values.
    +   * When enabled, this option:
    +   * - Escapes HTML special characters (`<`, `>`, `"`, `'`, `&`, `/`, `=`) in interpolation parameters
    +   * - Sanitizes the final translated HTML to prevent XSS attacks by:
    +   *   - Escaping dangerous characters in HTML attribute values
    +   *   - Neutralizing event handler attributes (onclick, onerror, etc.)
    +   *   - Disabling javascript: URLs in href, src, action, formaction, and style attributes
        *
        * This is useful when translation output is used in `v-html` and the translation resource contains html markup (e.g. <b> around a user provided value).
        *
        * This usage pattern mostly occurs when passing precomputed text strings into UI components.
        *
    -   * The escape process involves replacing the following symbols with their respective HTML character entities: `<`, `>`, `"`, `'`.
    -   *
        * Setting `escapeParameter` as true should not break existing functionality but provides a safeguard against a subtle type of XSS attack vectors.
        *
    -   * @VueI18nSee [HTML Message](../guide/essentials/syntax#html-message)
    +   * @VueI18nSee [HTML Message - Using the escapeParameter option](../guide/essentials/syntax#using-the-escapeparameter-option)
        *
        * @defaultValue `false`
        */
    
  • packages/vue-i18n-core/src/legacy.ts+8 4 modified
    @@ -248,17 +248,21 @@ export interface VueI18nOptions<
       warnHtmlInMessage?: WarnHtmlInMessageLevel
       /**
        * @remarks
    -   * If `escapeParameterHtml` is configured as true then interpolation parameters are escaped before the message is translated.
    +   * Whether to escape parameters for list or named interpolation values.
    +   * When enabled, this option:
    +   * - Escapes HTML special characters (`<`, `>`, `"`, `'`, `&`, `/`, `=`) in interpolation parameters
    +   * - Sanitizes the final translated HTML to prevent XSS attacks by:
    +   *   - Escaping dangerous characters in HTML attribute values
    +   *   - Neutralizing event handler attributes (onclick, onerror, etc.)
    +   *   - Disabling javascript: URLs in href, src, action, formaction, and style attributes
        *
        * This is useful when translation output is used in `v-html` and the translation resource contains html markup (e.g. <b> around a user provided value).
        *
        * This usage pattern mostly occurs when passing precomputed text strings into UI components.
        *
    -   * The escape process involves replacing the following symbols with their respective HTML character entities: `<`, `>`, `"`, `'`.
    -   *
        * Setting `escapeParameterHtml` as true should not break existing functionality but provides a safeguard against a subtle type of XSS attack vectors.
        *
    -   * @VueI18nSee [HTML Message](../guide/essentials/syntax#html-message)
    +   * @VueI18nSee [HTML Message - Using the escapeParameter option](../guide/essentials/syntax#using-the-escapeparameter-option)
        *
        * @defaultValue `false`
        */
    

Vulnerability mechanics

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

References

10

News mentions

0

No linked articles in our index yet.