fix: verify URL when viewing dashboards

This commit is contained in:
Fine 2025-07-17 17:43:28 +08:00
parent f472d551b6
commit 1b486935bb
3 changed files with 70 additions and 51 deletions

View File

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

View File

@ -29,6 +29,7 @@ limitations under the License. -->
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { ref } from "vue"; import { ref } from "vue";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { validateAndSanitizeUrl } from "@/utils/validateAndSanitizeUrl";
const { t } = useI18n(); const { t } = useI18n();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
@ -37,54 +38,6 @@ limitations under the License. -->
const url = ref(widget.url || ""); const url = ref(widget.url || "");
const urlError = ref(""); 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() { function handleUrlChange() {
const validation = validateAndSanitizeUrl(url.value); const validation = validateAndSanitizeUrl(url.value);
urlError.value = validation.error; urlError.value = validation.error;

View File

@ -31,8 +31,8 @@ limitations under the License. -->
</div> </div>
<div class="body"> <div class="body">
<iframe <iframe
v-if="widget.url" v-if="widgetUrl.isValid"
:src="widget.url" :src="widgetUrl.sanitizedUrl"
width="100%" width="100%"
height="100%" height="100%"
scrolling="no" scrolling="no"
@ -40,7 +40,7 @@ limitations under the License. -->
sandbox="allow-scripts allow-same-origin" sandbox="allow-scripts allow-same-origin"
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
></iframe> ></iframe>
<div v-else class="tips">{{ t("iframeWidgetTip") }}</div> <div v-else class="tips">{{ widgetUrl.error || t("iframeWidgetTip") }}</div>
</div> </div>
</div> </div>
</template> </template>
@ -49,6 +49,7 @@ limitations under the License. -->
import type { PropType } from "vue"; import type { PropType } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { validateAndSanitizeUrl } from "@/utils/validateAndSanitizeUrl";
/*global defineProps */ /*global defineProps */
const props = defineProps({ const props = defineProps({
@ -61,6 +62,7 @@ limitations under the License. -->
const { t } = useI18n(); const { t } = useI18n();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const widget = computed(() => props.data.widget || {}); const widget = computed(() => props.data.widget || {});
const widgetUrl = computed(() => validateAndSanitizeUrl(widget.value.url || ""));
function removeTopo() { function removeTopo() {
dashboardStore.removeControls(props.data); dashboardStore.removeControls(props.data);