CVE-2021-4231 (TypeScript, published 2022-05-26T14:15Z)
Description: value: A vulnerability was found in Angular up to 11.0.4/11.1.0-next.2. It has been classified as problematic. Affected is the handling of comments. The manipulation leads to cross site scripting. It is possible to launch the attack remotely but it might require an authentication first. Upgrading to version 11.0.5 and 11.1.0-next.3 is able to address this issue. The name of the patch is ba8da742e3b243e8f43d4c63aa842b44e14f2b09. It is recommended to upgrade the affected component.
Severity: LOW (CVSS v3 base score: 5.4)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/angular/angular
Files changed:
packages/core/src/util/dom.ts
packages/core/test/acceptance/security_spec.ts
packages/core/test/util/dom_spec.ts
Commit message: fix(core): fix possible XSS attack in development through SSR (#40525)

This is a follow up fix for
https://github.com/angular/angular/pull/40136/commits/894286dd0c92b5af223364237e63798e18b14f58.

It turns out that comments can be closed in several ways:
- `<!-->`
- `<!-- -->`
- `<!-- --!>`

All of the above are valid ways to close comment per:
https://html.spec.whatwg.org/multipage/syntax.html#comments

The new fix surrounds `<` and `>` with zero width space so that it
renders in the same way, but it prevents the comment to be closed eagerly.

PR Close #40525


Vulnerable code:
const END_COMMENT = /-->/g;
const END_COMMENT_ESCAPED = '-\u200B-\u200B>';
 * Escape the content of the strings so that it can be safely inserted into a comment node.
 * `<!-- The way you close a comment is with "-->". -->`. Above the `"-->"` is meant to be text not
 * an end to the comment. This can be created programmatically through DOM APIs.
 * This function escapes the comment text by looking for the closing char sequence `-->` and replace
 * it with `-_-_>` where the `_` is a zero width space `\u200B`. The result is that if a comment
 * contains `-->` text it will render normally but it will not cause the HTML parser to close the
 * comment.
 * @param value text to make safe for comment node by escaping the comment close character sequence
  return value.replace(END_COMMENT, END_COMMENT_ESCAPED);
  it('should not be possible to do XSS through comment reflect data', () => {
    @Component({template: `<div><span *ngIf="xssValue"></span><div>`})
    class XSSComp {
      xssValue: string = '--> --><script>"evil"</script>';
    }
    TestBed.configureTestingModule({declarations: [XSSComp]});
    const fixture = TestBed.createComponent(XSSComp);
    fixture.detectChanges();
    const div = fixture.nativeElement.querySelector('div') as HTMLElement;
    // Serialize into a string to mimic SSR serialization.
    const html = div.innerHTML;
    // This must be escaped or we have XSS.
    expect(html).not.toContain('--><script');
    // Now parse it back into DOM (from string)
    div.innerHTML = html;
    // Verify that we did not accidentally deserialize the `<script>`
    const script = div.querySelector('script');
    expect(script).toBeFalsy();
      expect(escapeCommentText('before-->after')).toEqual('before-\u200b-\u200b>after');
          .toEqual('before-\u200b-\u200b>inline-\u200b-\u200b>after');

Fixed code:
/**
 * Disallowed strings in the comment.
 *
 * see: https://html.spec.whatwg.org/multipage/syntax.html#comments
 */
const COMMENT_DISALLOWED = /^>|^->|<!--|-->|--!>|<!-$/g;
/**
 * Delimiter in the disallowed strings which needs to be wrapped with zero with character.
 */
const COMMENT_DELIMITER = /(<|>)/;
const COMMENT_DELIMITER_ESCAPED = '\u200B$1\u200B';
 * Escape the content of comment strings so that it can be safely inserted into a comment node.
 * Consider: `<!-- The way you close a comment is with ">", and "->" at the beginning or by "-->" or
 * "--!>" at the end. -->`. Above the `"-->"` is meant to be text not an end to the comment. This
 * can be created programmatically through DOM APIs. (`<!--` are also disallowed.)
 *
 * see: https://html.spec.whatwg.org/multipage/syntax.html#comments
 * This function escapes the comment text by looking for comment delimiters (`<` and `>`) and
 * surrounding them with `_>_` where the `_` is a zero width space `\u200B`. The result is that if a
 * comment contains any of the comment start/end delimiters (such as `<!--`, `-->` or `--!>`) the
 * text it will render normally but it will not cause the HTML parser to close/open the comment.
 * @param value text to make safe for comment node by escaping the comment open/close character
 *     sequence.
  return value.replace(
      COMMENT_DISALLOWED, (text) => text.replace(COMMENT_DELIMITER, COMMENT_DELIMITER_ESCAPED));
  // see: https://html.spec.whatwg.org/multipage/syntax.html#comments
  ['>',         // self closing
   '-->',       // standard closing
   '--!>',      // alternate closing
   '<!-- -->',  // embedded comment.
  ].forEach((xssValue) => {
    it('should not be possible to do XSS through comment reflect data when writing: ' + xssValue,
       () => {
         @Component({template: `<div><span *ngIf="xssValue"></span><div>`})
         class XSSComp {
           // ngIf serializes the `xssValue` into a comment for debugging purposes.
           xssValue: string = xssValue + '<script>"evil"</script>';
         }
         TestBed.configureTestingModule({declarations: [XSSComp]});
         const fixture = TestBed.createComponent(XSSComp);
         fixture.detectChanges();
         const div = fixture.nativeElement.querySelector('div') as HTMLElement;
         // Serialize into a string to mimic SSR serialization.
         const html = div.innerHTML;
         // This must be escaped or we have XSS.
         expect(html).not.toContain('--><script');
         // Now parse it back into DOM (from string)
         div.innerHTML = html;
         // Verify that we did not accidentally deserialize the `<script>`
         const script = div.querySelector('script');
         expect(script).toBeFalsy();
       });
    it('should escape "<" or ">"', () => {
      expect(escapeCommentText('<!--')).toEqual('\u200b<\u200b!--');
      expect(escapeCommentText('<!--<!--')).toEqual('\u200b<\u200b!--\u200b<\u200b!--');
      expect(escapeCommentText('>')).toEqual('\u200b>\u200b');
      expect(escapeCommentText('>-->')).toEqual('\u200b>\u200b--\u200b>\u200b');
    });

      expect(escapeCommentText('before-->after')).toEqual('before--\u200b>\u200bafter');
          .toEqual('before--\u200b>\u200binline--\u200b>\u200bafter');
    });

    it('should caver the spec', () => {
      // https://html.spec.whatwg.org/multipage/syntax.html#comments
      expect(escapeCommentText('>')).toEqual('\u200b>\u200b');
      expect(escapeCommentText('->')).toEqual('-\u200b>\u200b');
      expect(escapeCommentText('<!--')).toEqual('\u200b<\u200b!--');
      expect(escapeCommentText('-->')).toEqual('--\u200b>\u200b');
      expect(escapeCommentText('--!>')).toEqual('--!\u200b>\u200b');
      expect(escapeCommentText('<!-')).toEqual('\u200b<\u200b!-');

      // Things which are OK
      expect(escapeCommentText('.>')).toEqual('.>');
      expect(escapeCommentText('.->')).toEqual('.->');
      expect(escapeCommentText('<!-.')).toEqual('<!-.');