CVE-2022-1432 (JavaScript, published 2022-05-18T14:15Z)
Description: value: Cross-site Scripting (XSS) - Generic in GitHub repository octoprint/octoprint prior to 1.8.0.
Severity: MEDIUM (CVSS v3 base score: 6.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/octoprint/octoprint
Files changed:
src/octoprint/static/js/app/helpers.js
src/octoprint/static/js/app/viewmodels/settings.js
src/octoprint/templates/snippets/settings/webcam/webcamStreamUrl.jinja2
Commit message: 🔒️ Fix XSS in webcam stream test


Vulnerable code:
var determineWebcamStreamType = function (streamUrl) {
    if (streamUrl) {
        if (streamUrl.startsWith("webrtc")) {
            return "webrtc";
        }
        var lastDotPosition = streamUrl.lastIndexOf(".");
        var firstQuotationSignPosition = streamUrl.indexOf("?");
        if (
            lastDotPosition != -1 &&
            firstQuotationSignPosition != -1 &&
            lastDotPosition >= firstQuotationSignPosition
        ) {
            throw "Malformed URL. Cannot determine stream type.";
        }
        // If we have found a dot, try to extract the extension.
        if (lastDotPosition > -1) {
            if (firstQuotationSignPosition > -1) {
                var extension = streamUrl.slice(
                    lastDotPosition + 1,
                    firstQuotationSignPosition - 1
                );
            } else {
                var extension = streamUrl.slice(lastDotPosition + 1);
            }
            if (extension.toLowerCase() == "m3u8") {
                return "hls";
            }
        }
        // By default, 'mjpg' is the stream type.
        return "mjpg";
            var streamType = self.webcam_streamType();
            if (streamType == "mjpg") {
            } else if (streamType == "hls") {
            } else if (isWebRTCAvailable() && streamType == "webrtc") {
<div class="control-group" title="{{ _('URL to embed into the UI for live viewing of the webcam stream')|edq }}">
            <button class="btn" type="button" data-bind="click: testWebcamStreamUrl, enable: webcam_streamUrl() && !testWebcamStreamUrlBusy(), css: {disabled: !webcam_streamUrl() || testWebcamStreamUrlBusy()}"><i class="fas fa-spinner fa-spin" data-bind="visible: testWebcamStreamUrlBusy"></i> {{ _('Test') }}</button>

Fixed code:
var getExternalHostUrl = function () {
    var loc = window.location;
    var port = "";
    if (
        (loc.protocol === "http:" && loc.port !== "80") ||
        (loc.protocol === "https:" && loc.port !== "443")
    ) {
        port = ":" + loc.port;
    }
    return loc.protocol + "//" + loc.hostname + port;
};
var validateWebcamUrl = function (streamUrl) {
    if (!streamUrl) {
        return false;
    }
    var lower = streamUrl.toLowerCase();
    var toParse = streamUrl;

    if (lower.startsWith("//")) {
        // protocol relative
        toParse = window.location.protocol + streamUrl;
    } else if (lower.startsWith("/")) {
        // host relative
        toParse = getExternalHostUrl() + streamUrl;
    } else if (
        lower.startsWith("http:") ||
        lower.startsWith("https:") ||
        lower.startsWith("webrtc:")
    ) {
        // absolute & http/https/webrtc
        toParse = streamUrl;
        return false;
    }

    try {
        return new URL(toParse);
    } catch (e) {
        return false;
    }
};

var determineWebcamStreamType = function (streamUrl) {
    if (!streamUrl) {

    var parsed = validateWebcamUrl(streamUrl);
    if (!parsed) {
        throw "Invalid streamUrl. Cannot determine stream type.";
    }

    if (parsed.protocol === "webrtc:") {
        return "webrtc";
    }

    var lastDotPosition = parsed.pathname.lastIndexOf(".");
    if (lastDotPosition !== -1) {
        var extension = parsed.pathname.substring(lastDotPosition + 1);
        if (extension.toLowerCase() === "m3u8") {
            return "hls";
        }
    }

    // By default, 'mjpg' is the stream type.
    return "mjpg";
        self.webcam_streamValid = ko.pureComputed(function () {
            var url = self.webcam_streamUrl();
            return !url || validateWebcamUrl(url);
        });

            var streamType;
            try {
                streamType = self.webcam_streamType();
            } catch (e) {
                streamType = "";
            }

            if (streamType === "mjpg") {
            } else if (streamType === "hls") {
            } else if (isWebRTCAvailable() && streamType === "webrtc") {
<div class="control-group" title="{{ _('URL to embed into the UI for live viewing of the webcam stream')|edq }}" data-bind="css: { error: !webcam_streamValid() }">
            <button class="btn" type="button" data-bind="click: testWebcamStreamUrl, enable: !testWebcamStreamUrlBusy() && webcam_streamValid(), css: {disabled: testWebcamStreamUrlBusy() || !webcam_streamValid()}"><i class="fas fa-spinner fa-spin" data-bind="visible: testWebcamStreamUrlBusy"></i> {{ _('Test') }}</button>