CVE-2020-7676
Description
angular.js prior to 1.8.0 allows cross site scripting. The regex-based input HTML replacement may turn sanitized code into unsanitized one. Wrapping "" elements in "" ones changes parsing behavior, leading to possibly unsanitizing code.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
AngularJS versions prior to 1.8.0 are vulnerable to cross-site scripting via a regex-based HTML sanitization bypass when elements are wrapped in .
Vulnerability
Description
CVE-2020-7676 is a cross-site scripting (XSS) vulnerability in AngularJS (angular.js) affecting versions prior to 1.8.0. The root cause lies in the regex-based HTML sanitization that incorrectly handles ` elements when they are wrapped inside ` elements. This wrapping changes the parsing behavior, allowing an attacker to bypass the sanitization and inject malicious code [1][2].
Exploitation
An attacker can exploit this vulnerability by providing crafted HTML input that includes ` tags within ` tags. The regex-based replacement logic fails to properly sanitize the content, leading to the injection of arbitrary HTML or JavaScript. No authentication is required, and the attack can be executed through any user-controlled input that is processed by AngularJS's HTML sanitizer [4].
Impact
Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of the victim's browser, potentially leading to session hijacking, credential theft, or defacement. The vulnerability is classified as medium severity (CVSS 6.1) and is particularly dangerous because AngularJS was widely used in web applications [1].
Mitigation
The vulnerability is fixed in AngularJS version 1.8.0 [2]. Users are strongly advised to upgrade to this version or later. Note that AngularJS has reached end-of-life as of January 2022, so no further security updates will be provided [3]. If upgrade is not possible, consider replacing AngularJS with a supported framework.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
angularnpm | < 1.8.0 | 1.8.0 |
Affected products
2- angular.js/angular.jsdescription
Patches
12df43c077791fix(jqLite): prevent possible XSS due to regex-based HTML replacement
5 files changed · +177 −15
src/Angular.js+21 −0 modified@@ -93,6 +93,7 @@ hasOwnProperty, createMap, stringify, + UNSAFE_restoreLegacyJqLiteXHTMLReplacement, NODE_TYPE_ELEMENT, NODE_TYPE_ATTRIBUTE, @@ -1949,6 +1950,26 @@ function bindJQuery() { bindJQueryFired = true; } +/** + * @ngdoc function + * @name angular.UNSAFE_restoreLegacyJqLiteXHTMLReplacement + * @module ng + * @kind function + * + * @description + * Restores the pre-1.8 behavior of jqLite that turns XHTML-like strings like + * `<div /><span />` to `<div></div><span></span>` instead of `<div><span></span></div>`. + * The new behavior is a security fix so if you use this method, please try to adjust + * to the change & remove the call as soon as possible. + + * Note that this only patches jqLite. If you use jQuery 3.5.0 or newer, please read + * [jQuery 3.5 upgrade guide](https://jquery.com/upgrade-guide/3.5/) for more details + * about the workarounds. + */ +function UNSAFE_restoreLegacyJqLiteXHTMLReplacement() { + JQLite.legacyXHTMLReplacement = true; +} + /** * throw error if the argument is falsy. */
src/AngularPublic.js+1 −0 modified@@ -156,6 +156,7 @@ function publishExternalAPI(angular) { 'callbacks': {$$counter: 0}, 'getTestability': getTestability, 'reloadWithDebugInfo': reloadWithDebugInfo, + 'UNSAFE_restoreLegacyJqLiteXHTMLReplacement': UNSAFE_restoreLegacyJqLiteXHTMLReplacement, '$$minErr': minErr, '$$csp': csp, '$$encodeUriSegment': encodeUriSegment,
src/.eslintrc.json+1 −0 modified@@ -100,6 +100,7 @@ "VALIDITY_STATE_PROPERTY": false, "reloadWithDebugInfo": false, "stringify": false, + "UNSAFE_restoreLegacyJqLiteXHTMLReplacement": false, "NODE_TYPE_ELEMENT": false, "NODE_TYPE_ATTRIBUTE": false,
src/jqLite.js+58 −15 modified@@ -90,6 +90,16 @@ * - [`val()`](http://api.jquery.com/val/) * - [`wrap()`](http://api.jquery.com/wrap/) * + * jqLite also provides a method restoring pre-1.8 insecure treatment of XHTML-like tags + * that makes input like `<div /><span />` turned to `<div></div><span></span>` instead of + * `<div><span></span></div>` like version 1.8 & newer do: + * ```js + * angular.UNSAFE_restoreLegacyJqLiteXHTMLReplacement(); + * ``` + * Note that this only patches jqLite. If you use jQuery 3.5.0 or newer, please read + * [jQuery 3.5 upgrade guide](https://jquery.com/upgrade-guide/3.5/) for more details + * about the workarounds. + * * ## jQuery/jqLite Extras * AngularJS also provides the following additional methods and events to both jQuery and jqLite: * @@ -169,20 +179,36 @@ var HTML_REGEXP = /<|&#?\w+;/; var TAG_NAME_REGEXP = /<([\w:-]+)/; var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi; +// Table parts need to be wrapped with `<table>` or they're +// stripped to their contents when put in a div. +// XHTML parsers do not magically insert elements in the +// same way that tag soup parsers do, so we cannot shorten +// this by omitting <tbody> or other required elements. var wrapMap = { - 'option': [1, '<select multiple="multiple">', '</select>'], - - 'thead': [1, '<table>', '</table>'], - 'col': [2, '<table><colgroup>', '</colgroup></table>'], - 'tr': [2, '<table><tbody>', '</tbody></table>'], - 'td': [3, '<table><tbody><tr>', '</tr></tbody></table>'], - '_default': [0, '', ''] + thead: ['table'], + col: ['colgroup', 'table'], + tr: ['tbody', 'table'], + td: ['tr', 'tbody', 'table'] }; -wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; +// Support: IE <10 only +// IE 9 requires an option wrapper & it needs to have the whole table structure +// set up up front; assigning `"<td></td>"` to `tr.innerHTML` doesn't work, etc. +var wrapMapIE9 = { + option: [1, '<select multiple="multiple">', '</select>'], + _default: [0, '', ''] +}; + +for (var key in wrapMap) { + var wrapMapValueClosing = wrapMap[key]; + var wrapMapValue = wrapMapValueClosing.slice().reverse(); + wrapMapIE9[key] = [wrapMapValue.length, '<' + wrapMapValue.join('><') + '>', '</' + wrapMapValueClosing.join('></') + '>']; +} + +wrapMapIE9.optgroup = wrapMapIE9.option; function jqLiteIsTextNode(html) { return !HTML_REGEXP.test(html); @@ -203,7 +229,7 @@ function jqLiteHasData(node) { } function jqLiteBuildFragment(html, context) { - var tmp, tag, wrap, + var tmp, tag, wrap, finalHtml, fragment = context.createDocumentFragment(), nodes = [], i; @@ -214,13 +240,30 @@ function jqLiteBuildFragment(html, context) { // Convert html into DOM nodes tmp = fragment.appendChild(context.createElement('div')); tag = (TAG_NAME_REGEXP.exec(html) || ['', ''])[1].toLowerCase(); - wrap = wrapMap[tag] || wrapMap._default; - tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, '<$1></$2>') + wrap[2]; + finalHtml = JQLite.legacyXHTMLReplacement ? + html.replace(XHTML_TAG_REGEXP, '<$1></$2>') : + html; + + if (msie < 10) { + wrap = wrapMapIE9[tag] || wrapMapIE9._default; + tmp.innerHTML = wrap[1] + finalHtml + wrap[2]; + + // Descend through wrappers to the right content + i = wrap[0]; + while (i--) { + tmp = tmp.firstChild; + } + } else { + wrap = wrapMap[tag] || []; - // Descend through wrappers to the right content - i = wrap[0]; - while (i--) { - tmp = tmp.lastChild; + // Create wrappers & descend into them + i = wrap.length; + while (--i > -1) { + tmp.appendChild(window.document.createElement(wrap[i])); + tmp = tmp.firstChild; + } + + tmp.innerHTML = finalHtml; } nodes = concat(nodes, tmp.childNodes);
test/jqLiteSpec.js+96 −0 modified@@ -147,6 +147,13 @@ describe('jqLite', function() { expect(nodes[0].nodeName.toLowerCase()).toBe('option'); }); + it('should allow construction of multiple <option> elements', function() { + var nodes = jqLite('<option></option><option></option>'); + expect(nodes.length).toBe(2); + expect(nodes[0].nodeName.toLowerCase()).toBe('option'); + expect(nodes[1].nodeName.toLowerCase()).toBe('option'); + }); + // Special tests for the construction of elements which are restricted (in the HTML5 spec) to // being children of specific nodes. @@ -169,6 +176,95 @@ describe('jqLite', function() { expect(nodes[0].nodeName.toLowerCase()).toBe(name); }); }); + + describe('security', function() { + it('shouldn\'t crash at attempts to close the table wrapper', function() { + // jQuery doesn't pass this test yet. + if (!_jqLiteMode) return; + + // Support: IE <10 + // In IE 9 we still need to use the old-style innerHTML assignment + // as that's the only one that works. + if (msie < 10) return; + + expect(function() { + // This test case attempts to close the tags which wrap input + // based on matching done in wrapMap, escaping the wrapper & thus + // triggering an error when descending. + var el = jqLite('<td></td></tr></tbody></table><td></td>'); + expect(el.length).toBe(2); + expect(el[0].nodeName.toLowerCase()).toBe('td'); + expect(el[1].nodeName.toLowerCase()).toBe('td'); + }).not.toThrow(); + }); + + it('shouldn\'t unsanitize sanitized code', function(done) { + // jQuery <3.5.0 fail those tests. + if (isJQuery2x()) { + done(); + return; + } + + var counter = 0, + assertCount = 13, + container = jqLite('<div></div>'); + + function donePartial() { + counter++; + if (counter === assertCount) { + container.remove(); + delete window.xss; + done(); + } + } + + jqLite(document.body).append(container); + window.xss = jasmine.createSpy('xss'); + + // Thanks to Masato Kinugawa from Cure53 for providing the following test cases. + // Note: below test cases need to invoke the xss function with consecutive + // decimal parameters for the assertions to be correct. + forEach([ + '<img alt="<x" title="/><img src=url404 onerror=xss(0)>">', + '<img alt="\n<x" title="/>\n<img src=url404 onerror=xss(1)>">', + '<style><style/><img src=url404 onerror=xss(2)>', + '<xmp><xmp/><img src=url404 onerror=xss(3)>', + '<title><title /><img src=url404 onerror=xss(4)>', + '<iframe><iframe/><img src=url404 onerror=xss(5)>', + '<noframes><noframes/><img src=url404 onerror=xss(6)>', + '<noscript><noscript/><img src=url404 onerror=xss(7)>', + '<foo" alt="" title="/><img src=url404 onerror=xss(8)>">', + '<img alt="<x" title="" src="/><img src=url404 onerror=xss(9)>">', + '<noscript/><img src=url404 onerror=xss(10)>', + '<noembed><noembed/><img src=url404 onerror=xss(11)>', + + '<option><style></option></select><img src=url404 onerror=xss(12)></style>' + ], function(htmlString, index) { + var element = jqLite('<div></div>'); + + container.append(element); + element.append(jqLite(htmlString)); + + window.setTimeout(function() { + expect(window.xss).not.toHaveBeenCalledWith(index); + donePartial(); + }, 1000); + }); + }); + + it('should allow to restore legacy insecure behavior', function() { + // jQuery doesn't have this API. + if (!_jqLiteMode) return; + + // eslint-disable-next-line new-cap + angular.UNSAFE_restoreLegacyJqLiteXHTMLReplacement(); + + var elem = jqLite('<div/><span/>'); + expect(elem.length).toBe(2); + expect(elem[0].nodeName.toLowerCase()).toBe('div'); + expect(elem[1].nodeName.toLowerCase()).toBe('span'); + }); + }); }); describe('_data', function() {
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
26- github.com/advisories/GHSA-mhp6-pxh8-r675ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-7676ghsaADVISORY
- github.com/angular/angular.js/commit/2df43c07779137d1bddf7f3b282a1287a8634acdghsaWEB
- github.com/angular/angular.js/pull/17028ghsaWEB
- github.com/angular/angular.js/pull/17028%2Cmitrex_refsource_MISC
- lists.apache.org/thread.html/r198985c02829ba8285ed4f9b1de54a33b5f31b08bb38ac51fc86961b%40%3Cozone-issues.hadoop.apache.org%3Eghsamailing-listx_refsource_MLISTWEB
- lists.apache.org/thread.html/r198985c02829ba8285ed4f9b1de54a33b5f31b08bb38ac51fc86961b@%3Cozone-issues.hadoop.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r3f05cfd587c774ea83c18e59eda9fa37fa9bbf3421484d4ee1017a20%40%3Cozone-issues.hadoop.apache.org%3Eghsamailing-listx_refsource_MLISTWEB
- lists.apache.org/thread.html/r3f05cfd587c774ea83c18e59eda9fa37fa9bbf3421484d4ee1017a20@%3Cozone-issues.hadoop.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r446c297cd6cda2bd7e345c9b0741d7f611df89902e5d515848c6f4b1%40%3Cozone-issues.hadoop.apache.org%3Eghsamailing-listx_refsource_MLISTWEB
- lists.apache.org/thread.html/r446c297cd6cda2bd7e345c9b0741d7f611df89902e5d515848c6f4b1@%3Cozone-issues.hadoop.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r455ebd83a1c69ae8fd897560534a079c70a483dbe1e75504f1ca499b%40%3Cozone-issues.hadoop.apache.org%3Eghsamailing-listx_refsource_MLISTWEB
- lists.apache.org/thread.html/r455ebd83a1c69ae8fd897560534a079c70a483dbe1e75504f1ca499b@%3Cozone-issues.hadoop.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r57383582dcad2305430321589dfaca6793f5174c55da6ce8d06fbf9b%40%3Cozone-issues.hadoop.apache.org%3Eghsamailing-listx_refsource_MLISTWEB
- lists.apache.org/thread.html/r57383582dcad2305430321589dfaca6793f5174c55da6ce8d06fbf9b@%3Cozone-issues.hadoop.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r79e3feaaf87b81e80da0e17a579015f6dcb94c95551ced398d50c8d7%40%3Cozone-issues.hadoop.apache.org%3Eghsamailing-listx_refsource_MLISTWEB
- lists.apache.org/thread.html/r79e3feaaf87b81e80da0e17a579015f6dcb94c95551ced398d50c8d7@%3Cozone-issues.hadoop.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r80f210a5f4833d59c5d3de17dd7312f9daba0765ec7d4052469f13f1%40%3Cozone-commits.hadoop.apache.org%3Eghsamailing-listx_refsource_MLISTWEB
- lists.apache.org/thread.html/r80f210a5f4833d59c5d3de17dd7312f9daba0765ec7d4052469f13f1@%3Cozone-commits.hadoop.apache.org%3EghsaWEB
- lists.apache.org/thread.html/rb6423268b25db0f800359986867648e11dbd38e133b9383e85067f02%40%3Cozone-issues.hadoop.apache.org%3Eghsamailing-listx_refsource_MLISTWEB
- lists.apache.org/thread.html/rb6423268b25db0f800359986867648e11dbd38e133b9383e85067f02@%3Cozone-issues.hadoop.apache.org%3EghsaWEB
- lists.apache.org/thread.html/rda99599896c3667f2cc9e9d34c7b6ef5d2bbed1f4801e1d75a2b0679%40%3Ccommits.nifi.apache.org%3Eghsamailing-listx_refsource_MLISTWEB
- lists.apache.org/thread.html/rda99599896c3667f2cc9e9d34c7b6ef5d2bbed1f4801e1d75a2b0679@%3Ccommits.nifi.apache.org%3EghsaWEB
- lists.apache.org/thread.html/rfa2b19d01d10a8637dc319a7d5994c3dbdb88c0a8f9a21533403577a%40%3Cozone-issues.hadoop.apache.org%3Eghsamailing-listx_refsource_MLISTWEB
- lists.apache.org/thread.html/rfa2b19d01d10a8637dc319a7d5994c3dbdb88c0a8f9a21533403577a@%3Cozone-issues.hadoop.apache.org%3EghsaWEB
- snyk.io/vuln/SNYK-JS-ANGULAR-570058ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.