This is an automated email from the ASF dual-hosted git repository.

qiuxiafan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-booster-ui.git


The following commit(s) were added to refs/heads/main by this push:
     new 47cd6d22 fix: verify URL when viewing dashboards (#483)
47cd6d22 is described below

commit 47cd6d22c0e1dc3b772bd3766a2c1e4e97f3c68e
Author: Fine0830 <fanxue0...@gmail.com>
AuthorDate: Thu Jul 17 18:03:41 2025 +0800

    fix: verify URL when viewing dashboards (#483)
---
 src/utils/validateAndSanitizeUrl.ts                | 64 ++++++++++++++++++++++
 .../dashboard/configuration/ThirdPartyApp.vue      | 49 +----------------
 src/views/dashboard/controls/ThirdPartyApp.vue     |  8 ++-
 3 files changed, 70 insertions(+), 51 deletions(-)

diff --git a/src/utils/validateAndSanitizeUrl.ts 
b/src/utils/validateAndSanitizeUrl.ts
new file mode 100644
index 00000000..ab9edfa3
--- /dev/null
+++ b/src/utils/validateAndSanitizeUrl.ts
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// URL validation function to prevent XSS
+export function validateAndSanitizeUrl(inputUrl: string): { isValid: boolean; 
sanitizedUrl: string; error: string } {
+  if (!inputUrl.trim()) {
+    return { isValid: true, sanitizedUrl: "", error: "" };
+  }
+
+  try {
+    // Create URL object to validate the URL format
+    const urlObj = new URL(inputUrl);
+
+    // Only allow HTTP and HTTPS protocols to prevent XSS
+    if (!["http:", "https:"].includes(urlObj.protocol)) {
+      return {
+        isValid: false,
+        sanitizedUrl: "",
+        error: "Only HTTP and HTTPS URLs are allowed",
+      };
+    }
+
+    // Additional security checks
+    const dangerousProtocols = ["javascript:", "data:", "vbscript:", "le:"];
+    const lowerUrl = inputUrl.toLowerCase();
+
+    for (const protocol of dangerousProtocols) {
+      if (lowerUrl.includes(protocol)) {
+        return {
+          isValid: false,
+          sanitizedUrl: "",
+          error: "Dangerous protocols are not allowed",
+        };
+      }
+    }
+
+    // Return the sanitized URL (using the URL object to normalize it)
+    return {
+      isValid: true,
+      sanitizedUrl: urlObj.href,
+      error: "",
+    };
+  } catch (error) {
+    return {
+      isValid: false,
+      sanitizedUrl: "",
+      error: "Please enter a valid URL",
+    };
+  }
+}
diff --git a/src/views/dashboard/configuration/ThirdPartyApp.vue 
b/src/views/dashboard/configuration/ThirdPartyApp.vue
index 59f2721f..d8b4f4cb 100644
--- a/src/views/dashboard/configuration/ThirdPartyApp.vue
+++ b/src/views/dashboard/configuration/ThirdPartyApp.vue
@@ -29,6 +29,7 @@ limitations under the License. -->
   import { useI18n } from "vue-i18n";
   import { ref } from "vue";
   import { useDashboardStore } from "@/store/modules/dashboard";
+  import { validateAndSanitizeUrl } from "@/utils/validateAndSanitizeUrl";
 
   const { t } = useI18n();
   const dashboardStore = useDashboardStore();
@@ -37,54 +38,6 @@ limitations under the License. -->
   const url = ref(widget.url || "");
   const urlError = ref("");
 
-  // URL validation function to prevent XSS
-  function validateAndSanitizeUrl(inputUrl: string): { isValid: boolean; 
sanitizedUrl: string; error: string } {
-    if (!inputUrl.trim()) {
-      return { isValid: true, sanitizedUrl: "", error: "" };
-    }
-
-    try {
-      // Create URL object to validate the URL format
-      const urlObj = new URL(inputUrl);
-
-      // Only allow HTTP and HTTPS protocols to prevent XSS
-      if (!["http:", "https:"].includes(urlObj.protocol)) {
-        return {
-          isValid: false,
-          sanitizedUrl: "",
-          error: "Only HTTP and HTTPS URLs are allowed",
-        };
-      }
-
-      // Additional security checks
-      const dangerousProtocols = ["javascript:", "data:", "vbscript:", "le:"];
-      const lowerUrl = inputUrl.toLowerCase();
-
-      for (const protocol of dangerousProtocols) {
-        if (lowerUrl.includes(protocol)) {
-          return {
-            isValid: false,
-            sanitizedUrl: "",
-            error: "Dangerous protocols are not allowed",
-          };
-        }
-      }
-
-      // Return the sanitized URL (using the URL object to normalize it)
-      return {
-        isValid: true,
-        sanitizedUrl: urlObj.href,
-        error: "",
-      };
-    } catch (error) {
-      return {
-        isValid: false,
-        sanitizedUrl: "",
-        error: "Please enter a valid URL",
-      };
-    }
-  }
-
   function handleUrlChange() {
     const validation = validateAndSanitizeUrl(url.value);
     urlError.value = validation.error;
diff --git a/src/views/dashboard/controls/ThirdPartyApp.vue 
b/src/views/dashboard/controls/ThirdPartyApp.vue
index 6d6a0340..f37dab0f 100644
--- a/src/views/dashboard/controls/ThirdPartyApp.vue
+++ b/src/views/dashboard/controls/ThirdPartyApp.vue
@@ -31,8 +31,8 @@ limitations under the License. -->
     </div>
     <div class="body">
       <iframe
-        v-if="widget.url"
-        :src="widget.url"
+        v-if="widgetUrl.isValid"
+        :src="widgetUrl.sanitizedUrl"
         width="100%"
         height="100%"
         scrolling="no"
@@ -40,7 +40,7 @@ limitations under the License. -->
         sandbox="allow-scripts allow-same-origin"
         referrerpolicy="no-referrer"
       ></iframe>
-      <div v-else class="tips">{{ t("iframeWidgetTip") }}</div>
+      <div v-else class="tips">{{ widgetUrl.error || t("iframeWidgetTip") 
}}</div>
     </div>
   </div>
 </template>
@@ -49,6 +49,7 @@ limitations under the License. -->
   import type { PropType } from "vue";
   import { useI18n } from "vue-i18n";
   import { useDashboardStore } from "@/store/modules/dashboard";
+  import { validateAndSanitizeUrl } from "@/utils/validateAndSanitizeUrl";
 
   /*global defineProps */
   const props = defineProps({
@@ -61,6 +62,7 @@ limitations under the License. -->
   const { t } = useI18n();
   const dashboardStore = useDashboardStore();
   const widget = computed(() => props.data.widget || {});
+  const widgetUrl = computed(() => validateAndSanitizeUrl(widget.value.url || 
""));
 
   function removeTopo() {
     dashboardStore.removeControls(props.data);

Reply via email to