mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-08-02 13:50:39 +00:00
fix: restrict and validate url for widgets
This commit is contained in:
parent
5d311a41a2
commit
5e54f378a2
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user