mirror of
https://github.com/discourse/discourse.git
synced 2025-03-11 00:15:38 +08:00
SECURITY: Sanitize video placeholder urls
Make sure video placeholder urls are valid. An error message is displayed instead of an infinite loading spinner after clicking play.
This commit is contained in:
parent
2c5dbdc23f
commit
8192aedd69
@ -1,5 +1,6 @@
|
|||||||
import { spinnerHTML } from "discourse/helpers/loading-spinner";
|
import { spinnerHTML } from "discourse/helpers/loading-spinner";
|
||||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
|
import { sanitize } from "discourse/lib/text";
|
||||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||||
import discourseLater from "discourse-common/lib/later";
|
import discourseLater from "discourse-common/lib/later";
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
@ -15,10 +16,39 @@ export default {
|
|||||||
parentDiv.style.cursor = "";
|
parentDiv.style.cursor = "";
|
||||||
overlay.innerHTML = spinnerHTML;
|
overlay.innerHTML = spinnerHTML;
|
||||||
|
|
||||||
|
const videoSrc = sanitizeUrl(parentDiv.dataset.videoSrc);
|
||||||
|
const origSrc = sanitizeUrl(parentDiv.dataset.origSrc);
|
||||||
|
const dataOrigSrcAttr =
|
||||||
|
origSrc !== null ? `data-orig-src="${origSrc}"` : "";
|
||||||
|
|
||||||
|
if (videoSrc === null) {
|
||||||
|
const existingNotice = wrapper.querySelector(".notice.error");
|
||||||
|
if (existingNotice) {
|
||||||
|
existingNotice.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const notice = document.createElement("div");
|
||||||
|
notice.className = "notice error";
|
||||||
|
notice.innerHTML =
|
||||||
|
iconHTML("triangle-exclamation") +
|
||||||
|
" " +
|
||||||
|
I18n.t("invalid_video_url");
|
||||||
|
wrapper.appendChild(notice);
|
||||||
|
overlay.innerHTML = iconHTML("play");
|
||||||
|
|
||||||
|
parentDiv.style.cursor = "pointer";
|
||||||
|
parentDiv.addEventListener(
|
||||||
|
"click",
|
||||||
|
(e) => handleVideoPlaceholderClick(helper, e),
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const videoHTML = `
|
const videoHTML = `
|
||||||
<video width="100%" height="100%" preload="metadata" controls style="display:none">
|
<video width="100%" height="100%" preload="metadata" controls style="display:none">
|
||||||
<source src="${parentDiv.dataset.videoSrc}" ${parentDiv.dataset.origSrc}>
|
<source src="${videoSrc}" ${dataOrigSrcAttr}>
|
||||||
<a href="${parentDiv.dataset.videoSrc}">${parentDiv.dataset.videoSrc}</a>
|
<a href="${videoSrc}">${videoSrc}</a>
|
||||||
</video>`;
|
</video>`;
|
||||||
parentDiv.insertAdjacentHTML("beforeend", videoHTML);
|
parentDiv.insertAdjacentHTML("beforeend", videoHTML);
|
||||||
parentDiv.classList.add("video-container");
|
parentDiv.classList.add("video-container");
|
||||||
@ -108,6 +138,33 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeUrl(url) {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url, window.location.origin);
|
||||||
|
|
||||||
|
if (
|
||||||
|
["http:", "https:"].includes(parsedUrl.protocol) ||
|
||||||
|
url.startsWith("/")
|
||||||
|
) {
|
||||||
|
const sanitized = sanitize(url);
|
||||||
|
|
||||||
|
if (
|
||||||
|
sanitized &&
|
||||||
|
sanitized.trim() !== "" &&
|
||||||
|
!sanitized.includes(">") &&
|
||||||
|
!sanitized.includes("<")
|
||||||
|
) {
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn("Invalid URL encountered:", url, e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
api.decorateCookedElement(applyVideoPlaceholder, {
|
api.decorateCookedElement(applyVideoPlaceholder, {
|
||||||
onlyStream: true,
|
onlyStream: true,
|
||||||
});
|
});
|
||||||
|
@ -50,4 +50,30 @@ acceptance("Video Placeholder Test", function () {
|
|||||||
.hasStyle({ display: "block" }, "The video is no longer hidden");
|
.hasStyle({ display: "block" }, "The video is no longer hidden");
|
||||||
assert.dom(".video-placeholder-wrapper").doesNotExist();
|
assert.dom(".video-placeholder-wrapper").doesNotExist();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("displays an error for invalid video URL and allows retry", async function (assert) {
|
||||||
|
await visit("/t/54081");
|
||||||
|
|
||||||
|
const placeholder = document.querySelector(".video-placeholder-container");
|
||||||
|
placeholder.setAttribute(
|
||||||
|
"data-video-src",
|
||||||
|
'http://example.com/video.mp4"><script>alert(1)</script>'
|
||||||
|
);
|
||||||
|
|
||||||
|
await click(".video-placeholder-overlay");
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(".video-placeholder-wrapper .notice.error")
|
||||||
|
.exists("An error message is displayed for an invalid URL");
|
||||||
|
assert
|
||||||
|
.dom(".video-placeholder-wrapper .notice.error")
|
||||||
|
.hasText(
|
||||||
|
"This video cannot be played because the URL is invalid or unavailable.",
|
||||||
|
"Error message is correct"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom("video")
|
||||||
|
.doesNotExist("No video element is created for invalid URL");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -4360,6 +4360,7 @@ en:
|
|||||||
retry: "Retry loading the image"
|
retry: "Retry loading the image"
|
||||||
|
|
||||||
cannot_render_video: This video cannot be rendered because your browser does not support the codec.
|
cannot_render_video: This video cannot be rendered because your browser does not support the codec.
|
||||||
|
invalid_video_url: This video cannot be played because the URL is invalid or unavailable.
|
||||||
|
|
||||||
keyboard_shortcuts_help:
|
keyboard_shortcuts_help:
|
||||||
shortcut_key_delimiter_comma: ", "
|
shortcut_key_delimiter_comma: ", "
|
||||||
|
Loading…
x
Reference in New Issue
Block a user