CVE-2021-23443: Access of Resource Using Incompatible Type ('Type Confusion') (CWE-843)
value: This affects the package edge.js before 5.3.2. A type confusion vulnerability can be used to bypass input sanitization when the input to be rendered is an array (instead of a string or a SafeValue), even if {{ }} are used.
CWE description: The product allocates or initializes a resource such as a pointer, object, or variable using one type, but it later accesses that resource using a type that is incompatible with the original type.
CVSS v3 base score: 6.1
Published: 2021-09-21T17:15Z
Repository: https://github.com/edge-js/edge
Language: TypeScript
Files changed: src/Contracts/index.ts, src/Template/index.ts, test/template.spec.ts
Commit date: 2021-09-04 18:48:14 +0530
Commit message: fix: Cross-Site Scripting vulnerability reported by Snyk security team

Diff: diff --git a/src/Contracts/index.ts b/src/Contracts/index.ts
index 08e4959..c4e723d 100644
--- a/src/Contracts/index.ts
+++ b/src/Contracts/index.ts
@@ -205,11 +205,11 @@ export interface TemplateContract {
   ): T
 
   /**
    * Escape input
    */
-  escape<T>(input: T): T
+  escape(input: any): string
 
   /**
    * Rethrow exceptions by pointing back to edge source file and line number
    */
   reThrow(error: any, filename: string, line: number): never
diff --git a/src/Template/index.ts b/src/Template/index.ts
index 52ea4cd..fbc1145 100644
--- a/src/Template/index.ts
+++ b/src/Template/index.ts
@@ -26,16 +26,12 @@ export class SafeValue {
 }
 
 /**
  * Escapes a given string
  */
-export function escape<T>(input: T): T extends SafeValue ? T['value'] : T {
-  return typeof input === 'string'
-    ? string.escapeHTML(input)
-    : input instanceof SafeValue
-    ? input.value
-    : input
+export function escape(input: any): string {
+  return input instanceof SafeValue ? input.value : string.escapeHTML(String(input))
 }
 
 /**
  * Mark value as safe and not to be escaped
  */
@@ -195,11 +191,11 @@ export class Template extends Macroable implements TemplateContract {
 
   /**
    * Escapes the value to be HTML safe. Only strings are escaped
    * and rest all values will be returned as it is.
    */
-  public escape<T>(input: T): T extends SafeValue ? T['value'] : T {
+  public escape(input: any): string {
     return escape(input)
   }
 
   /**
    * Raise an error
diff --git a/test/template.spec.ts b/test/template.spec.ts
index ab1c064..49f92bc 100644
--- a/test/template.spec.ts
+++ b/test/template.spec.ts
@@ -138,24 +138,45 @@ test.group('Template', (group) => {
     const compiler = new Compiler(loader, tags, processor, { cache: false })
     const template = new Template(compiler, {}, {}, processor)
     assert.equal(template.escape('<h2> Hello world </h2>'), '&lt;h2&gt; Hello world &lt;/h2&gt;')
   })
 
-  test('do not escape values, which are not string', (assert) => {
+  test('stringify value during escape', (assert) => {
     const processor = new Processor()
     const compiler = new Compiler(loader, tags, processor, { cache: false })
     const template = new Template(compiler, {}, {}, processor)
-    assert.equal(template.escape(22), 22)
+    assert.equal(template.escape(22), '22')
   })
 
   test('do not escape values, which instance of safe value', (assert) => {
     const processor = new Processor()
     const compiler = new Compiler(loader, tags, processor, { cache: false })
     const template = new Template(compiler, {}, {}, processor)
     assert.equal(template.escape(safeValue('<h2> Hello world </h2>')), '<h2> Hello world </h2>')
   })
 
+  test('stringify array before escape', (assert) => {
+    const processor = new Processor()
+    const compiler = new Compiler(loader, tags, processor, { cache: false })
+    const template = new Template(compiler, {}, {}, processor)
+    assert.equal(template.escape(['<h2> Hello world </h2>']), '&lt;h2&gt; Hello world &lt;/h2&gt;')
+  })
+
+  test('stringify object before escape', (assert) => {
+    const processor = new Processor()
+    const compiler = new Compiler(loader, tags, processor, { cache: false })
+    const template = new Template(compiler, {}, {}, processor)
+    assert.equal(
+      template.escape({
+        toString() {
+          return '<h2> Hello world </h2>'
+        },
+      }),
+      '&lt;h2&gt; Hello world &lt;/h2&gt;'
+    )
+  })
+
   test('add macros to context', (assert) => {
     Template.macro('upper', (username) => {
       return username.toUpperCase()
     })
 

Vulnerable code:   escape<T>(input: T): T
export function escape<T>(input: T): T extends SafeValue ? T['value'] : T {
  return typeof input === 'string'
    ? string.escapeHTML(input)
    : input instanceof SafeValue
    ? input.value
    : input
  public escape<T>(input: T): T extends SafeValue ? T['value'] : T {
  test('do not escape values, which are not string', (assert) => {
    assert.equal(template.escape(22), 22)
Fixed code:   escape(input: any): string
export function escape(input: any): string {
  return input instanceof SafeValue ? input.value : string.escapeHTML(String(input))
  public escape(input: any): string {
  test('stringify value during escape', (assert) => {
    assert.equal(template.escape(22), '22')
  test('stringify array before escape', (assert) => {
    const processor = new Processor()
    const compiler = new Compiler(loader, tags, processor, { cache: false })
    const template = new Template(compiler, {}, {}, processor)
    assert.equal(template.escape(['<h2> Hello world </h2>']), '&lt;h2&gt; Hello world &lt;/h2&gt;')
  })

  test('stringify object before escape', (assert) => {
    const processor = new Processor()
    const compiler = new Compiler(loader, tags, processor, { cache: false })
    const template = new Template(compiler, {}, {}, processor)
    assert.equal(
      template.escape({
        toString() {
          return '<h2> Hello world </h2>'
        },
      }),
      '&lt;h2&gt; Hello world &lt;/h2&gt;'
    )
  })