Skip to content

Commit

Permalink
🔒️ Fix XSS in webcam stream test
Browse files Browse the repository at this point in the history
  • Loading branch information
foosel committed May 11, 2022
1 parent 5ef1677 commit 6d259d7
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 36 deletions.
93 changes: 63 additions & 30 deletions src/octoprint/static/js/app/helpers.js
Expand Up @@ -1542,41 +1542,74 @@ var copyToClipboard = function (text) {
temp.remove();
};

var determineWebcamStreamType = function (streamUrl) {
if (streamUrl) {
if (streamUrl.startsWith("webrtc")) {
return "webrtc";
}
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 lastDotPosition = streamUrl.lastIndexOf(".");
var firstQuotationSignPosition = streamUrl.indexOf("?");
if (
lastDotPosition != -1 &&
firstQuotationSignPosition != -1 &&
lastDotPosition >= firstQuotationSignPosition
) {
throw "Malformed URL. Cannot determine stream type.";
}
var validateWebcamUrl = function (streamUrl) {
if (!streamUrl) {
return false;
}

// 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 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;
} else {
return false;
}

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

var determineWebcamStreamType = function (streamUrl) {
if (!streamUrl) {
throw "Empty streamUrl. Cannot determine stream type.";
}

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";
};

var saveToLocalStorage = function (key, data) {
Expand Down
19 changes: 15 additions & 4 deletions src/octoprint/static/js/app/viewmodels/settings.js
Expand Up @@ -319,6 +319,10 @@ $(function () {
return "";
}
});
self.webcam_streamValid = ko.pureComputed(function () {
var url = self.webcam_streamUrl();
return !url || validateWebcamUrl(url);
});

self.server_onlineCheckText = ko.observable();
self.server_onlineCheckOk = ko.observable(false);
Expand Down Expand Up @@ -450,12 +454,19 @@ $(function () {
var text = gettext(
"If you see your webcam stream below, the entered stream URL is ok."
);
var streamType = self.webcam_streamType();

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

var webcam_element;
var webrtc_peer_connection;
if (streamType == "mjpg") {
if (streamType === "mjpg") {
webcam_element = $('<img src="' + self.webcam_streamUrl() + '">');
} else if (streamType == "hls") {
} else if (streamType === "hls") {
webcam_element = $(
'<video id="webcam_hls" muted autoplay style="width: 100%"/>'
);
Expand All @@ -467,7 +478,7 @@ $(function () {
hls.loadSource(self.webcam_streamUrl());
hls.attachMedia(video_element);
}
} else if (isWebRTCAvailable() && streamType == "webrtc") {
} else if (isWebRTCAvailable() && streamType === "webrtc") {
webcam_element = $(
'<video id="webcam_webrtc" muted autoplay playsinline controls style="width: 100%"/>'
);
Expand Down
@@ -1,9 +1,9 @@
<div class="control-group" title="{{ _('URL to embed into the UI for live viewing of the webcam stream')|edq }}">
<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() }">
<label class="control-label" for="settings-webcamStreamUrl">{{ _('Stream URL') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" class="input-block-level" data-bind="value: webcam_streamUrl, valueUpdate: 'afterkeydown'" id="settings-webcamStreamUrl">
<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>
<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>
</div>
<span class="help-block">
<p>{% trans %}Needs to be reachable from the browser displaying the OctoPrint UI, used to embed the webcam stream into the page.{% endtrans %}</p>
Expand Down

0 comments on commit 6d259d7

Please sign in to comment.