CVE-2018-1999024: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: MathJax version prior to version 2.7.4 contains a Cross Site Scripting (XSS) vulnerability in the \unicode{} macro that can result in Potentially untrusted Javascript running within a web browser. This attack appear to be exploitable via The victim must view a page where untrusted content is processed using Mathjax. This vulnerability appears to have been fixed in 2.7.4 and later.
CWE description: 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.
CVSS v3 base score: 5.4
Published: 2018-07-23T16:29Z
Repository: https://github.com/mathjax/MathJax
Language: JavaScript
Files changed: unpacked/extensions/TeX/unicode.js, unpacked/extensions/toMathML.js
Commit date: 2018-04-02 15:39:45 -0400
Commit message: Fix parsing and output for \class{} and \unicode{}

Diff: diff --git a/unpacked/extensions/TeX/unicode.js b/unpacked/extensions/TeX/unicode.js
index 3104b9df6..a1c946588 100644
--- a/unpacked/extensions/TeX/unicode.js
+++ b/unpacked/extensions/TeX/unicode.js
@@ -87,23 +87,26 @@ MathJax.Hub.Register.StartupHook("TeX Jax Ready",function () {
       if (HD) {
         if (HD.replace(/ /g,"").match(/^(\d+(\.\d*)?|\.\d+),(\d+(\.\d*)?|\.\d+)$/))
           {HD = HD.replace(/ /g,"").split(/,/); font = this.GetBrackets(name)}
             else {font = HD; HD = null}
       }
-      var n = this.trimSpaces(this.GetArgument(name)),
-          N = parseInt(n.match(/^x/) ? "0"+n : n);
+      var n = this.trimSpaces(this.GetArgument(name)).replace(/^0x/,"x");
+      if (!n.match(/^(x[0-9A-Fa-f]+|[0-9]+)$/)) {
+        TEX.Error(["BadUnicode","Argument to \\unicode must be a number"]);
+      }
+      var N = parseInt(n.match(/^x/) ? "0"+n : n);
       if (!UNICODE[N]) {UNICODE[N] = [800,200,font,N]}
       else if (!font) {font = UNICODE[N][2]}
       if (HD) {
         UNICODE[N][0] = Math.floor(HD[0]*1000);
         UNICODE[N][1] = Math.floor(HD[1]*1000);
       }
       var variant = this.stack.env.font, def = {};
       if (font) {
         UNICODE[N][2] = def.fontfamily = font.replace(/"/g,"'");
         if (variant) {
-          if (variant.match(/bold/))   {def.fontweight = "bold"}
+          if (variant.match(/bold/)) {def.fontweight = "bold"}
           if (variant.match(/italic|-mathit/)) {def.fontstyle = "italic"}
         }
       } else if (variant) {def.mathvariant = variant}
       def.unicode = [].concat(UNICODE[N]); // make a copy
       this.Push(MML.mtext(MML.entity("#"+n)).With(def));
diff --git a/unpacked/extensions/toMathML.js b/unpacked/extensions/toMathML.js
index ead3eb5d2..989a3b113 100644
--- a/unpacked/extensions/toMathML.js
+++ b/unpacked/extensions/toMathML.js
@@ -84,11 +84,11 @@ MathJax.Hub.Register.LoadHook("[MathJax]/jax/element/mml/jax.js",function () {
         }
       }
       if (this.mathvariant && this.toMathMLvariants[this.mathvariant])
         {CLASS.push("MJX"+this.mathvariant)}
       if (this.variantForm) {CLASS.push("MJX-variant")}
-      if (CLASS.length) {attr.unshift('class="'+CLASS.join(" ")+'"')}
+      if (CLASS.length) {attr.unshift('class="'+this.toMathMLquote(CLASS.join(" "))+'"')}
     },
     toMathMLattribute: function (value) {
       if (typeof(value) === "string" &&
           value.replace(/ /g,"").match(/^(([-+])?(\d+(\.\d*)?|\.\d+))mu$/)) {
         // FIXME:  should take scriptlevel into account
@@ -163,11 +163,11 @@ MathJax.Hub.Register.LoadHook("[MathJax]/jax/element/mml/jax.js",function () {
         if (nested) {data.unshift(space+"    <mrow>"); data.push(space+"    </mrow>")}
         data.unshift(space+"  <semantics>");
         var xmlEscapedTex = jax.originalText.replace(/[&<>]/g, function(item) {
             return { '>': '&gt;', '<': '&lt;','&': '&amp;' }[item]
         });
-        data.push(space+'    <annotation encoding="'+annotation+'">'+xmlEscapedTex+"</annotation>");
+        data.push(space+'    <annotation encoding="'+this.toMathMLquote(annotation)+'">'+xmlEscapedTex+"</annotation>");
         data.push(space+"  </semantics>");
       }
       return space+"<"+tag+attr+">\n"+data.join("\n")+"\n"+space+"</"+tag+">";
     }
   });
@@ -219,11 +219,11 @@ MathJax.Hub.Register.LoadHook("[MathJax]/jax/element/mml/jax.js",function () {
   MML.chars.Augment({
     toMathML: function (space) {return (space||"") + this.toMathMLquote(this.toString())}
   });
   
   MML.entity.Augment({
-    toMathML: function (space) {return (space||"") + "&"+this.data[0]+";<!-- "+this.toString()+" -->"}
+    toMathML: function (space) {return (space||"") + "&"+this.toMathMLquote(this.data[0])+";<!-- "+this.toString()+" -->"}
   });
   
   MML.xml.Augment({
    toMathML: function (space) {return (space||"") + this.toString()}
   });

Vulnerable code:       var n = this.trimSpaces(this.GetArgument(name)),
          N = parseInt(n.match(/^x/) ? "0"+n : n);
          if (variant.match(/bold/))   {def.fontweight = "bold"}
      if (CLASS.length) {attr.unshift('class="'+CLASS.join(" ")+'"')}
        data.push(space+'    <annotation encoding="'+annotation+'">'+xmlEscapedTex+"</annotation>");
    toMathML: function (space) {return (space||"") + "&"+this.data[0]+";<!-- "+this.toString()+" -->"}
Fixed code:       var n = this.trimSpaces(this.GetArgument(name)).replace(/^0x/,"x");
      if (!n.match(/^(x[0-9A-Fa-f]+|[0-9]+)$/)) {
        TEX.Error(["BadUnicode","Argument to \\unicode must be a number"]);
      }
      var N = parseInt(n.match(/^x/) ? "0"+n : n);
          if (variant.match(/bold/)) {def.fontweight = "bold"}
      if (CLASS.length) {attr.unshift('class="'+this.toMathMLquote(CLASS.join(" "))+'"')}
        data.push(space+'    <annotation encoding="'+this.toMathMLquote(annotation)+'">'+xmlEscapedTex+"</annotation>");
    toMathML: function (space) {return (space||"") + "&"+this.toMathMLquote(this.data[0])+";<!-- "+this.toString()+" -->"}