CVE-2024-36043 (published 2024-05-18T20:15Z)

question_image.ts in SurveyJS Form Library before 1.10.4 allows contentMode=youtube XSS via the imageLink property.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/surveyjs/survey-library
commit_details: repo_url: https://github.com/surveyjs/survey-library
author: Aleksey Novikov
author_date: 2024-05-13 19:27:40+03:00
msg: Image with contentMode: "youtube" allows external script execution
fixed https://github.com/surveyjs/service/issues/2174
repository: repo_url: https://github.com/surveyjs/survey-library
repo_name: surveyjs/survey-library
description: Free JavaScript form builder library with integration for React, Angular, Vue, jQuery, and Knockout.
repo_language: TypeScript
owner: surveyjs
file_changes: filename: question_image.ts
change_type: ModificationType.MODIFY
diff: @@ -5,7 +5,7 @@ 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";
@@ -13,8 +13,9 @@ 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;
 }
@@ -31,7 +32,7 @@ export class QuestionImageModel extends QuestionNonValue {
     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());
@@ -203,8 +204,9 @@ export class QuestionImageModel extends QuestionNonValue {
   }
 }
 
-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 = "";

programming_language: TypeScript
filename: question_imagetests.ts
change_type: ModificationType.MODIFY
diff: @@ -33,6 +33,18 @@ 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";
@@ -47,6 +59,24 @@ QUnit.test("Check NOT youtube video rendered mode", function (assert) {
   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 = {

programming_language: TypeScript