CVE-2021-32798 (JavaScript, published 2021-08-09T21:15Z)
Description: value: The Jupyter notebook is a web-based notebook environment for interactive computing. In affected versions untrusted notebook can execute code on load. Jupyter Notebook uses a deprecated version of Google Caja to sanitize user inputs. A public Caja bypass can be used to trigger an XSS when a victim opens a malicious ipynb document in Jupyter Notebook. The XSS allows an attacker to execute arbitrary code on the victim computer using Jupyter APIs.
Severity: MEDIUM (CVSS v3 base score: 9.6)
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/jupyter/notebook
Files changed:
bower.json
notebook/static/base/js/namespace.js
notebook/static/base/js/security.js
package.json
setupbase.py
webpack.config.js
Commit message: Merge pull request from GHSA-hwvq-6gjx-j797

* sanitizer fix

* Pass sanitizer options explicitly

Vulnerable code:
    "google-caja": "5669",
    Jupyter.version = "6.4.0";
    'components/google-caja/html-css-sanitizer-minified',
], function($, sanitize) {
    
    
    var caja;
    if (window && window.html) {
        caja = window.html;
        caja.html4 = window.html4;
        caja.sanitizeStylesheet = window.sanitizeStylesheet;
    }
    
    var sanitizeAttribs = function (tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) {
        /**
         * add trusting data-attributes to the default sanitizeAttribs from caja
         * this function is mostly copied from the caja source
         */
        var ATTRIBS = caja.html4.ATTRIBS;
        for (var i = 0; i < attribs.length; i += 2) {
            var attribName = attribs[i];
            if (attribName.substr(0,5) == 'data-') {
                var attribKey = '*::' + attribName;
                if (!ATTRIBS.hasOwnProperty(attribKey)) {
                    ATTRIBS[attribKey] = 0;
                }
            }
        }
        // Caja doesn't allow data uri for img::src, see
        // https://github.com/google/caja/issues/1558
        // This is not a security issue for browser post ie6 though, so we
        // disable the check
        // https://www.owasp.org/index.php/Script_in_IMG_tags
        ATTRIBS['img::src'] = 0;
        return caja.sanitizeAttribs(tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger);
    };
    
    var sanitize_css = function (css, tagPolicy) {
        /**
         * sanitize CSS
         * like sanitize_html, but for CSS
         * called by sanitize_stylesheets
         */
        return caja.sanitizeStylesheet(
            window.location.pathname,
            css,
            {
                containerClass: null,
                idSuffix: '',
                tagPolicy: tagPolicy,
                virtualizeAttrName: noop
            },
            noop
        );
    };
    
    var sanitize_stylesheets = function (html, tagPolicy) {
        /**
         * sanitize just the css in style tags in a block of html
         * called by sanitize_html, if allow_css is true
         */
        var h = $("<div/>").append(html);
        var style_tags = h.find("style");
        if (!style_tags.length) {
            // no style tags to sanitize
            return html;
        }
        style_tags.each(function(i, style) {
            style.innerHTML = sanitize_css(style.innerHTML, tagPolicy);
        });
        return h.html();
    };
    
        var html4 = caja.html4;

        if (allow_css) {
            // allow sanitization of style tags,
            // not just scrubbing
            html4.ELEMENTS.style &= ~html4.eflags.UNSAFE;
            html4.ATTRIBS.style = html4.atype.STYLE;
        } else {
            // scrub all CSS
            html4.ELEMENTS.style |= html4.eflags.UNSAFE;
            html4.ATTRIBS.style = html4.atype.SCRIPT;
        }
        
        var record_messages = function (msg, opts) {
            console.log("HTML Sanitizer", msg, opts);
        };
        
        var policy = function (tagName, attribs) {
            if (!(html4.ELEMENTS[tagName] & html4.eflags.UNSAFE)) {
                return {
                    'attribs': sanitizeAttribs(tagName, attribs,
                        noop, noop, record_messages)
                    };
            } else {
                record_messages(tagName + " removed", {
                  change: "removed",
                  tagName: tagName
                });
            }
        };
        
        var sanitized = caja.sanitizeWithPolicy(html, policy);
        
        if (allow_css) {
            // sanitize style tags as stylesheets
            sanitized = sanitize_stylesheets(sanitized, policy);
        }
        
        return sanitized;
    
        caja: caja,
    "requirejs": "^2.3.6"
        pjoin(components, "google-caja", "html-css-sanitizer-minified.js"),
        
        return mtime(self.bower_dir) < mtime(pjoin(repo_root, 'bower.json'))

Fixed code:
    Jupyter.version = "6.5.0.dev0";
    'components/sanitizer/index',
], function($, sanitizer) {

    var defaultSanitizer = sanitizer.defaultSanitizer;

         const options = {};
         if (!allow_css) {
             options.allowedStyles = {};
         }
        return defaultSanitizer.sanitize(html, options);

    "build:webpack": "webpack --mode development",
    "@jupyterlab/apputils": "^3.1.3",
    "requirejs": "^2.3.6",
    "webpack": "^5.46.0",
    "webpack-cli": "^4.7.2"
        pjoin(components, "sanitizer", "index.js"),
    sanitizer_dir = pjoin(bower_dir, 'sanitizer')
        if not os.path.exists(self.sanitizer_dir):
            return True

        bower_stale = mtime(self.bower_dir) < mtime(pjoin(repo_root, 'bower.json'))
        if bower_stale:
            return True

        return mtime(self.sanitizer_dir) < mtime(pjoin(repo_root, 'webpack.config.js'))
        if not os.path.exists(self.sanitizer_dir):
            run(['npm', 'run', 'build:webpack'], cwd=repo_root, env=env)
const path = require('path');

module.exports = {
  entry: '@jupyterlab/apputils/lib/sanitizer',
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'notebook/static/components/sanitizer'),
    libraryTarget: "amd"
  }
}