VYPR
Moderate severityOSV Advisory· Published Jan 27, 2026· Updated Jan 27, 2026

Hono has a Cross-site Scripting vulnerability

CVE-2026-24771

Description

Hono is a Web application framework that provides support for any JavaScript runtime. Prior to version 4.11.7, a Cross-Site Scripting (XSS) vulnerability exists in the ErrorBoundary component of the hono/jsx library. Under certain usage patterns, untrusted user-controlled strings may be rendered as raw HTML, allowing arbitrary script execution in the victim's browser. Version 4.11.7 patches the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
hononpm
< 4.11.74.11.7

Affected products

1
  • Range: v0.0.1, v0.0.10, v0.0.11, …

Patches

1
2cf60046d730

Merge commit from fork

https://github.com/honojs/honoTaku AmanoJan 27, 2026via ghsa
2 files changed · +150 11
  • src/jsx/components.test.tsx+109 0 modified
    @@ -86,6 +86,67 @@ describe('ErrorBoundary', () => {
           errorBoundaryCounter--
           suspenseCounter--
         })
    +
    +    it('string content', async () => {
    +      const html = <ErrorBoundary fallback={<Fallback />}>{'< ok >'}</ErrorBoundary>
    +
    +      expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; ok &gt;')
    +
    +      errorBoundaryCounter--
    +      suspenseCounter--
    +    })
    +
    +    it('error: string content', async () => {
    +      const html = (
    +        <ErrorBoundary fallback={'< error >'}>
    +          <Component error={true} />
    +        </ErrorBoundary>
    +      )
    +
    +      expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
    +
    +      errorBoundaryCounter--
    +      suspenseCounter--
    +    })
    +
    +    it('error: Promise<string> from fallback', async () => {
    +      const html = (
    +        <ErrorBoundary fallback={Promise.resolve('< error >')}>
    +          <Component error={true} />
    +        </ErrorBoundary>
    +      )
    +
    +      expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
    +
    +      errorBoundaryCounter--
    +      suspenseCounter--
    +    })
    +
    +    it('error: string content from fallbackRender', async () => {
    +      const html = (
    +        <ErrorBoundary fallbackRender={() => '< error >'}>
    +          <Component error={true} />
    +        </ErrorBoundary>
    +      )
    +
    +      expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
    +
    +      errorBoundaryCounter--
    +      suspenseCounter--
    +    })
    +
    +    it('error: Promise<string> from fallbackRender', async () => {
    +      const html = (
    +        <ErrorBoundary fallbackRender={() => Promise.resolve('< error >')}>
    +          <Component error={true} />
    +        </ErrorBoundary>
    +      )
    +
    +      expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
    +
    +      errorBoundaryCounter--
    +      suspenseCounter--
    +    })
       })
     
       describe('async', async () => {
    @@ -123,6 +184,54 @@ describe('ErrorBoundary', () => {
     
           suspenseCounter--
         })
    +
    +    it('error: string content', async () => {
    +      const html = (
    +        <ErrorBoundary fallback={'< error >'}>
    +          <Component error={true} />
    +        </ErrorBoundary>
    +      )
    +
    +      expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
    +
    +      suspenseCounter--
    +    })
    +
    +    it('error: Promise<string> from fallback', async () => {
    +      const html = (
    +        <ErrorBoundary fallback={Promise.resolve('< error >')}>
    +          <Component error={true} />
    +        </ErrorBoundary>
    +      )
    +
    +      expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
    +
    +      suspenseCounter--
    +    })
    +
    +    it('error: string content from fallbackRender', async () => {
    +      const html = (
    +        <ErrorBoundary fallbackRender={() => '< error >'}>
    +          <Component error={true} />
    +        </ErrorBoundary>
    +      )
    +
    +      expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
    +
    +      suspenseCounter--
    +    })
    +
    +    it('error: Promise<string> from fallbackRender', async () => {
    +      const html = (
    +        <ErrorBoundary fallbackRender={() => Promise.resolve('< error >')}>
    +          <Component error={true} />
    +        </ErrorBoundary>
    +      )
    +
    +      expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
    +
    +      suspenseCounter--
    +    })
       })
     
       describe('async : nested', async () => {
    
  • src/jsx/components.ts+41 11 modified
    @@ -1,6 +1,7 @@
     import { raw } from '../helper/html'
     import type { HtmlEscapedCallback, HtmlEscapedString } from '../utils/html'
     import { HtmlEscapedCallbackPhase, resolveCallback } from '../utils/html'
    +import { jsx, Fragment } from './base'
     import { DOM_RENDERER } from './constants'
     import { useContext } from './context'
     import { ErrorBoundary as ErrorBoundaryDomRenderer } from './dom/components'
    @@ -25,6 +26,21 @@ export const childrenToString = async (children: Child[]): Promise<HtmlEscapedSt
       }
     }
     
    +const resolveChildEarly = (c: Child): HtmlEscapedString | Promise<HtmlEscapedString> => {
    +  if (c == null || typeof c === 'boolean') {
    +    return '' as HtmlEscapedString
    +  } else if (typeof c === 'string') {
    +    return c as HtmlEscapedString
    +  } else {
    +    const str = c.toString()
    +    if (!(str instanceof Promise)) {
    +      return raw(str)
    +    } else {
    +      return str as Promise<HtmlEscapedString>
    +    }
    +  }
    +}
    +
     export type ErrorHandler = (error: Error) => void
     export type FallbackRender = (error: Error) => Child
     
    @@ -51,37 +67,51 @@ export const ErrorBoundary: FC<
       const nonce = useContext(StreamingContext)?.scriptNonce
     
       let fallbackStr: string | undefined
    -  const fallbackRes = (error: Error): HtmlEscapedString => {
    +  const resolveFallbackStr = async () => {
    +    const awaitedFallback = await fallback
    +    if (typeof awaitedFallback === 'string') {
    +      fallbackStr = awaitedFallback
    +    } else {
    +      fallbackStr = await awaitedFallback?.toString()
    +      if (typeof fallbackStr === 'string') {
    +        // should not apply `raw` if fallbackStr is undefined, null, or boolean
    +        fallbackStr = raw(fallbackStr)
    +      }
    +    }
    +  }
    +  const fallbackRes = (error: Error): HtmlEscapedString | Promise<HtmlEscapedString> => {
         onError?.(error)
    -    return (fallbackStr || fallbackRender?.(error) || '').toString() as HtmlEscapedString
    +    return (fallbackStr ||
    +      (fallbackRender && jsx(Fragment, {}, fallbackRender(error) as HtmlEscapedString)) ||
    +      '') as HtmlEscapedString
       }
       let resArray: HtmlEscapedString[] | Promise<HtmlEscapedString[]>[] = []
       try {
    -    resArray = children.map((c) =>
    -      c == null || typeof c === 'boolean' ? '' : c.toString()
    -    ) as HtmlEscapedString[]
    +    resArray = children.map(resolveChildEarly) as unknown as HtmlEscapedString[]
       } catch (e) {
    -    fallbackStr = await fallback?.toString()
    +    await resolveFallbackStr()
         if (e instanceof Promise) {
           resArray = [
             e.then(() => childrenToString(children as Child[])).catch((e) => fallbackRes(e)),
           ] as Promise<HtmlEscapedString[]>[]
         } else {
    -      resArray = [fallbackRes(e as Error)]
    +      resArray = [fallbackRes(e as Error) as HtmlEscapedString]
         }
       }
     
       if (resArray.some((res) => (res as {}) instanceof Promise)) {
    -    fallbackStr ||= await fallback?.toString()
    +    await resolveFallbackStr()
         const index = errorBoundaryCounter++
         const replaceRe = RegExp(`(<template id="E:${index}"></template>.*?)(.*?)(<!--E:${index}-->)`)
         const caught = false
    -    const catchCallback = ({ error, buffer }: { error: Error; buffer?: [string] }) => {
    +    const catchCallback = async ({ error, buffer }: { error: Error; buffer?: [string] }) => {
           if (caught) {
             return ''
           }
     
    -      const fallbackResString = fallbackRes(error)
    +      const fallbackResString = await Fragment({
    +        children: fallbackRes(error),
    +      }).toString()
           if (buffer) {
             buffer[0] = buffer[0].replace(replaceRe, fallbackResString)
           }
    @@ -195,7 +225,7 @@ d.remove()
           },
         ])
       } else {
    -    return raw(resArray.join(''))
    +    return Fragment({ children: resArray as Child[] })
       }
     }
     ;(ErrorBoundary as HasRenderToDom)[DOM_RENDERER] = ErrorBoundaryDomRenderer
    

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

4

News mentions

0

No linked articles in our index yet.