CVE-2024-36043: Insufficient Information (NVD-CWE-noinfo)
value: question_image.ts in SurveyJS Form Library before 1.10.4 allows contentMode=youtube XSS via the imageLink property.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-18T20:15Z
Repository: https://github.com/surveyjs/survey-library
Language: TypeScript
Files changed: src/question_image.ts, tests/question_imagetests.ts
Commit date: 2024-05-13 19:27:40 +0300
Commit message: Image with contentMode: "youtube" allows external script execution
fixed https://github.com/surveyjs/service/issues/2174

Diff: diff --git a/src/question_image.ts b/src/question_image.ts
index 5d2750664..96c05643c 100644
--- a/src/question_image.ts
+++ b/src/question_image.ts
@@ -3,20 +3,21 @@ import { property, Serializer } from "./jsonobject";
 import { QuestionFactory } from "./questionfactory";
 import { LocalizableString } from "./localizablestring";
 import { CssClassBuilder } from "./utils/cssClassBuilder";
 import { getRenderedStyleSize, getRenderedSize } from "./utils/utils";
 
-const youtubeTags = ["youtube.com", "youtu.be"];
+const youtubeDomains = ["www.youtube.com", "m.youtube.com", "youtube.com", "youtu.be"];
 const videoSuffics = [".mp4", ".mov", ".wmv", ".flv", ".avi", ".mkv"];
 const youtubeUrl = "https://www.youtube.com/";
 const youtubeEmbed = "embed";
 
 function isUrlYoutubeVideo(url: string): boolean {
   if (!url) return false;
   url = url.toLowerCase();
-  for (let i = 0; i < youtubeTags.length; i++) {
-    if (url.indexOf(youtubeTags[i]) !== -1) return true;
+  url = url.replace(/^https?:\/\//, "");
+  for (let i = 0; i < youtubeDomains.length; i++) {
+    if (url.indexOf(youtubeDomains[i] + "/") === 0) return true;
   }
   return false;
 }
 
 /**
@@ -29,11 +30,11 @@ export class QuestionImageModel extends QuestionNonValue {
 
   constructor(name: string) {
     super(name);
     const locImageLink = this.createLocalizableString("imageLink", this, false);
     locImageLink.onGetTextCallback = (text: string): string => {
-      return getCorrectImageLink(text);
+      return getCorrectImageLink(text, this.contentMode == "youtube");
     };
     this.createLocalizableString("altText", this, false);
     this.registerPropertyChangedHandlers(["contentMode", "imageLink"], () => this.calculateRenderedMode());
   }
   public getType(): string {
@@ -201,12 +202,13 @@ export class QuestionImageModel extends QuestionNonValue {
     }
     return false;
   }
 }
 
-function getCorrectImageLink(val: string): string {
-  if(!val || !isUrlYoutubeVideo(val)) return val;
+function getCorrectImageLink(val: string, isYouTube: boolean): string {
+  if (!val || !isUrlYoutubeVideo(val)) return isYouTube ? "" : val;
+  //if(!val || !isUrlYoutubeVideo(val)) return val;
   let res = val.toLocaleLowerCase();
   if(res.indexOf(youtubeEmbed) > -1) return val;
   let id = "";
   for(var i = val.length - 1; i >= 0; i --) {
     if(val[i] === "=" || val[i] === "/") break;
diff --git a/tests/question_imagetests.ts b/tests/question_imagetests.ts
index 93d1e57c2..94224e6c0 100644
--- a/tests/question_imagetests.ts
+++ b/tests/question_imagetests.ts
@@ -31,10 +31,22 @@ QUnit.test("Check video rendered mode onSurveyLoading", function (assert) {
 });
 QUnit.test("Check youtube video rendered mode", function (assert) {
   const question = new QuestionImageModel("q1");
   question.imageLink = "https://www.youtube.com/embed/tgbNymZ7vqY";
   assert.equal(question.renderedMode, "youtube");
+  question.imageLink = "https://youtube.com/embed/tgbNymZ7vqY";
+  assert.equal(question.renderedMode, "youtube");
+  question.imageLink = "m.youtube.com/embed/tgbNymZ7vqY";
+  assert.equal(question.renderedMode, "youtube");
+  question.imageLink = "youtube.com/embed/tgbNymZ7vqY";
+  assert.equal(question.renderedMode, "youtube");
+  question.imageLink = "youtu.be/tgbNymZ7vqY";
+  assert.equal(question.renderedMode, "youtube");
+  question.imageLink = "youtu.bee/tgbNymZ7vqY";
+  assert.equal(question.renderedMode, "image");
+  question.imageLink = "javascript:(alert('youtu.be'))";
+  assert.equal(question.renderedMode, "image");
   question.imageLink = "abcd";
   assert.equal(question.renderedMode, "image");
   question.imageLink = "https://youtu.be/tgbNymZ7vqY";
   assert.equal(question.renderedMode, "youtube");
 });
@@ -45,10 +57,28 @@ QUnit.test("Check NOT youtube video rendered mode", function (assert) {
   question.imageLink = "abcd";
   assert.equal(question.renderedMode, "image");
   question.imageLink = "videoUrl.avi";
   assert.equal(question.renderedMode, "video");
 });
+QUnit.test("Check youtube video imagelink", function (assert) {
+  const question = new QuestionImageModel("q1");
+  question.contentMode = "youtube";
+  question.imageLink = "https://www.youtube.com/embed/tgbNymZ7vqY";
+  assert.equal(question.locImageLink.renderedHtml, "https://www.youtube.com/embed/tgbNymZ7vqY");
+  question.imageLink = "javascript:alert('a')";
+  assert.equal(question.locImageLink.renderedHtml, "");
+  question.imageLink = "https://youtu.be/tgbNymZ7vqY";
+  assert.equal(question.locImageLink.renderedHtml, "https://www.youtube.com/embed/tgbNymZ7vqY");
+  question.imageLink = "javascript:alert('youtube.com')";
+  assert.equal(question.locImageLink.renderedHtml, "");
+  question.imageLink = "youtube.com.org";
+  assert.equal(question.locImageLink.renderedHtml, "");
+
+  question.contentMode = "image";
+  question.imageLink = "videoUrl.mov";
+  assert.equal(question.locImageLink.renderedHtml, "videoUrl.mov");
+});
 
 QUnit.test("Image adaptive mode", function (assert) {
   const json = {
     questions: [
       {

Vulnerable code: const youtubeTags = ["youtube.com", "youtu.be"];
  for (let i = 0; i < youtubeTags.length; i++) {
    if (url.indexOf(youtubeTags[i]) !== -1) return true;
      return getCorrectImageLink(text);
function getCorrectImageLink(val: string): string {
  if(!val || !isUrlYoutubeVideo(val)) return val;
Fixed code: const youtubeDomains = ["www.youtube.com", "m.youtube.com", "youtube.com", "youtu.be"];
  url = url.replace(/^https?:\/\//, "");
  for (let i = 0; i < youtubeDomains.length; i++) {
    if (url.indexOf(youtubeDomains[i] + "/") === 0) return true;
      return getCorrectImageLink(text, this.contentMode == "youtube");
function getCorrectImageLink(val: string, isYouTube: boolean): string {
  if (!val || !isUrlYoutubeVideo(val)) return isYouTube ? "" : val;
  //if(!val || !isUrlYoutubeVideo(val)) return val;
  question.imageLink = "https://youtube.com/embed/tgbNymZ7vqY";
  assert.equal(question.renderedMode, "youtube");
  question.imageLink = "m.youtube.com/embed/tgbNymZ7vqY";
  assert.equal(question.renderedMode, "youtube");
  question.imageLink = "youtube.com/embed/tgbNymZ7vqY";
  assert.equal(question.renderedMode, "youtube");
  question.imageLink = "youtu.be/tgbNymZ7vqY";
  assert.equal(question.renderedMode, "youtube");
  question.imageLink = "youtu.bee/tgbNymZ7vqY";
  assert.equal(question.renderedMode, "image");
  question.imageLink = "javascript:(alert('youtu.be'))";
  assert.equal(question.renderedMode, "image");
QUnit.test("Check youtube video imagelink", function (assert) {
  const question = new QuestionImageModel("q1");
  question.contentMode = "youtube";
  question.imageLink = "https://www.youtube.com/embed/tgbNymZ7vqY";
  assert.equal(question.locImageLink.renderedHtml, "https://www.youtube.com/embed/tgbNymZ7vqY");
  question.imageLink = "javascript:alert('a')";
  assert.equal(question.locImageLink.renderedHtml, "");
  question.imageLink = "https://youtu.be/tgbNymZ7vqY";
  assert.equal(question.locImageLink.renderedHtml, "https://www.youtube.com/embed/tgbNymZ7vqY");
  question.imageLink = "javascript:alert('youtube.com')";
  assert.equal(question.locImageLink.renderedHtml, "");
  question.imageLink = "youtube.com.org";
  assert.equal(question.locImageLink.renderedHtml, "");

  question.contentMode = "image";
  question.imageLink = "videoUrl.mov";
  assert.equal(question.locImageLink.renderedHtml, "videoUrl.mov");
});