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

View File

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

View File

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