fix: restrict and validate url for widgets (#480)

This commit is contained in:
Fine0830 2025-07-17 10:41:15 +08:00 committed by GitHub
parent 5d311a41a2
commit 1421f95ad3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 86 additions and 5 deletions

View File

@ -150,6 +150,7 @@ $theme-background: var(--theme-background);
$active-background: var(--el-color-primary);
$font-size-smaller: 12px;
$font-size-normal: 14px;
$error-color: #e66;
.opt:hover {
background-color: var(--sw-list-hover) !important;
@ -208,9 +209,9 @@ div.vis-tooltip {
}
.vis-item.Error {
background-color: #e66;
background-color: $error-color;
opacity: 0.8;
border-color: #e66;
border-color: $error-color;
color: var(--sw-event-vis-selected) !important;
}

View File

@ -13,13 +13,14 @@ limitations under the License. -->
<template>
<div class="item">
<span class="label">{{ t("iframeSrc") }}</span>
<el-input class="input" v-model="url" size="small" @change="changeConfig({ url: encodeURIComponent(url) })" />
<el-input class="input" v-model="url" size="small" @change="handleUrlChange" :class="{ error: urlError }" />
<div v-if="urlError" class="error-message">{{ urlError }}</div>
</div>
<div class="footer">
<el-button size="small" @click="cancelConfig">
{{ t("cancel") }}
</el-button>
<el-button size="small" type="primary" @click="applyConfig">
<el-button size="small" type="primary" @click="applyConfig" :disabled="!!urlError">
{{ t("apply") }}
</el-button>
</div>
@ -28,24 +29,89 @@ limitations under the License. -->
import { useI18n } from "vue-i18n";
import { ref } from "vue";
import { useDashboardStore } from "@/store/modules/dashboard";
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const originConfig = dashboardStore.selectedGrid;
const widget = originConfig.widget || {};
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;
if (validation.isValid) {
changeConfig({ url: validation.sanitizedUrl });
}
}
function changeConfig(param: { [key: string]: string }) {
const key = Object.keys(param)[0];
if (!key) {
return;
}
const { selectedGrid } = dashboardStore;
const widget = {
...dashboardStore.selectedGrid.widget,
[key]: decodeURIComponent(param[key]),
[key]: param[key], // Use the sanitized URL directly, no need for decodeURIComponent
};
dashboardStore.selectWidget({ ...selectedGrid, widget });
}
function applyConfig() {
if (urlError.value) {
return; // Don't apply if there's a validation error
}
dashboardStore.setConfigPanel(false);
dashboardStore.setConfigs(dashboardStore.selectedGrid);
}
@ -76,6 +142,18 @@ limitations under the License. -->
margin-bottom: 10px;
}
.url-input.error {
:deep(.el-input__inner) {
border-color: $error-color;
}
}
.error-message {
color: $error-color;
font-size: 12px;
margin-top: 4px;
}
.footer {
position: fixed;
bottom: 0;

View File

@ -37,6 +37,8 @@ limitations under the License. -->
height="100%"
scrolling="no"
style="border: none"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
referrerpolicy="no-referrer"
></iframe>
<div v-else class="tips">{{ t("iframeWidgetTip") }}</div>
</div>