diff --git a/lib/client/src/api/cached-promise.js b/lib/client/src/api/cached-promise.js
index b213a6517..5ee419768 100644
--- a/lib/client/src/api/cached-promise.js
+++ b/lib/client/src/api/cached-promise.js
@@ -9,6 +9,8 @@
* specific language governing permissions and limitations under the License.
*/
+import { isSafePropertyKey } from '../../../shared/security/property-injection-guard'
+
export default class CachedPromise {
constructor () {
this.cache = {}
@@ -37,7 +39,12 @@ export default class CachedPromise {
* @return {Promise} promise 对象
*/
set (id, promise) {
- Object.assign(this.cache, { [id]: promise })
+ // 验证id是否安全,防止属性注入攻击
+ if (!isSafePropertyKey(id)) {
+ console.warn(`[Security] Rejected unsafe cache key: ${id}`)
+ return
+ }
+ this.cache[id] = promise
}
/**
diff --git a/lib/client/src/api/index.js b/lib/client/src/api/index.js
index a8f59f738..a28faf300 100644
--- a/lib/client/src/api/index.js
+++ b/lib/client/src/api/index.js
@@ -29,6 +29,8 @@ axios.interceptors.response.use(
// 接口请求成功
case 0:
return data
+ case 200:
+ return data
// 需要去权限中心申请权限
case 403:
if (data.data.permissionType === 'page') {
diff --git a/lib/client/src/api/pureAxios.js b/lib/client/src/api/pureAxios.js
index b05defaf0..16aa45456 100644
--- a/lib/client/src/api/pureAxios.js
+++ b/lib/client/src/api/pureAxios.js
@@ -105,7 +105,7 @@ async function getPromise (method, url, data, userConfig = {}) {
try {
const response = await axiosRequest
Object.assign(config, response.config || {})
- if (Object.prototype.hasOwnProperty.call(response, 'code') && response.code !== 0) {
+ if (Object.prototype.hasOwnProperty.call(response, 'code') && response.code !== 0 && response.code !== 200) {
reject(response)
} else {
handleResponse({ config, response, resolve, reject })
diff --git a/lib/client/src/common/util.js b/lib/client/src/common/util.js
index 3b62a71de..825db23bf 100644
--- a/lib/client/src/common/util.js
+++ b/lib/client/src/common/util.js
@@ -10,7 +10,7 @@
*/
import { messageSuccess } from '@/common/bkmagic'
-import DOMPurify from 'dompurify'
+import { filterXss } from '@blueking/xss-filter'
import Vue from 'vue'
import LC from '@/element-materials/core'
import store from '@/store'
@@ -34,11 +34,15 @@ export function downloadFile (source, filename = 'lesscode.txt') {
const downloadEl = document.createElement('a')
const blob = new Blob([source])
downloadEl.download = filename
- downloadEl.href = URL.createObjectURL(blob)
+ const url = URL.createObjectURL(blob)
+ downloadEl.href = url
downloadEl.style.display = 'none'
document.body.appendChild(downloadEl)
downloadEl.click()
document.body.removeChild(downloadEl)
+ setTimeout(() => {
+ URL.revokeObjectURL(url)
+ }, 100)
}
/**
@@ -1013,5 +1017,5 @@ export const isJSON = (str) => {
export const filterImgSrc = (src) => {
src?.replaceAll('console', '')?.replaceAll('logout', '')
- return DOMPurify.sanitize(src)
+ return filterXss(src)
}
diff --git a/lib/client/src/components/code-viewer/index.vue b/lib/client/src/components/code-viewer/index.vue
index 75b473d64..8c589a690 100644
--- a/lib/client/src/components/code-viewer/index.vue
+++ b/lib/client/src/components/code-viewer/index.vue
@@ -46,6 +46,7 @@
import screenfull from 'screenfull'
import monaco from '@/components/monaco'
import { mapGetters } from 'vuex'
+ import { downloadFile } from '@/common/util'
export default {
components: {
@@ -108,14 +109,7 @@
this.$emit('show-edit-data')
},
handleDownloadFile () {
- const downlondEl = document.createElement('a')
- const blob = new Blob([this.code])
- downlondEl.download = this.filename
- downlondEl.href = URL.createObjectURL(blob)
- downlondEl.style.display = 'none'
- document.body.appendChild(downlondEl)
- downlondEl.click()
- document.body.removeChild(downlondEl)
+ downloadFile(this.code, this.filename)
},
handleScreenfull () {
const el = document.querySelector(`.${this.$style['code-viewer']}`)
diff --git a/lib/client/src/components/methods/forms/form-items/market.vue b/lib/client/src/components/methods/forms/form-items/market.vue
index 83aab422d..1acb22dc7 100644
--- a/lib/client/src/components/methods/forms/form-items/market.vue
+++ b/lib/client/src/components/methods/forms/form-items/market.vue
@@ -31,6 +31,7 @@
import { transformTipsWidth } from '@/common/util'
import mixins from './form-item-mixins'
import { mapActions } from 'vuex'
+ import { filterXss } from '@blueking/xss-filter'
export default {
mixins: [mixins],
@@ -59,7 +60,10 @@
...tips
}
}
- return tipObj
+ return {
+ ...tipObj,
+ content: filterXss(tipObj.content)
+ }
},
getMarketFuncs (isExpand) {
diff --git a/lib/client/src/components/patch/widget-bk-vision/index.vue b/lib/client/src/components/patch/widget-bk-vision/index.vue
index 6662e8265..540792d90 100644
--- a/lib/client/src/components/patch/widget-bk-vision/index.vue
+++ b/lib/client/src/components/patch/widget-bk-vision/index.vue
@@ -19,41 +19,21 @@
},
waterMark: {
type: String
- },
- isFullScroll: {
- type: Boolean,
- default: true
- },
- isShowTools: {
- type: Boolean,
- default: true
- },
- isShowRefresh: {
- type: Boolean,
- default: true
- },
- isShowTimeRange: {
- type: Boolean,
- default: true
}
},
data () {
return {
- // visionApp: {},
+ visionApp: {},
renderId: '',
apiPrefix: '/api/bkvision/',
- cdnPrefix: 'https://staticfile.qq.com/bkvision/p8e3a7f52d95c45d795cb6f90955f2800/latest/'
+ cdnPrefix: 'https://staticfile.qq.com/bkvision/pbb9b207ba200407982a9bd3d3f2895d4/latest/'
}
},
computed: {
dataInfo () {
return {
- apiPrefix: '/api/bkvision/'
- // waterMark: { content: this.watchMark || 'bk-lesscode' },
- // isFullScroll: this.isFullScroll,
- // isShowTools: this.isShowTools,
- // isShowRefresh: this.isShowRefresh,
- // isShowTimeRange: this.isShowTimeRange
+ apiPrefix: '/api/bkvision/',
+ waterMark: { content: this.watchMark || 'bk-lesscode' },
}
}
@@ -64,15 +44,6 @@
},
waterMark () {
this.debounceRender()
- },
- isShowTools () {
- this.debounceRender()
- },
- isShowRefresh () {
- this.debounceRender()
- },
- isShowTimeRange () {
- this.debounceRender()
}
},
created () {
@@ -91,11 +62,9 @@
methods: {
async loadSdk () {
const link = document.createElement('link')
- link.href = 'https://staticfile.qq.com/bkvision/p8e3a7f52d95c45d795cb6f90955f2800/3c3de519287048dcb4c5a03d47ebf33f/main.css'
+ link.href = 'https://staticfile.qq.com/bkvision/pbb9b207ba200407982a9bd3d3f2895d4/3c3de519287048dcb4c5a03d47ebf33f/main.css'
link.rel = 'stylesheet'
document.body.append(link)
- await this.loadScript('chunk-vendors.js')
- await this.loadScript('chunk-bk-magic-vue.js')
await this.loadScript('main.js')
this.initPanel()
},
@@ -112,8 +81,18 @@
})
},
async initPanel () {
+ console.log('init panel')
if (window.BkVisionSDK) {
- this.visionApp = this.uid && window.BkVisionSDK.init(`#dashboard-${this.renderId}`, this.uid, this.dataInfo)
+ try {
+ if (this.visionApp && Object.keys(this.visionApp).length) {
+ this.visionApp?.unmount()
+ }
+ } catch (error) {
+ console.error(error?.message || error, 'unmount bk-vision error')
+ }
+
+ this.visionApp = this.uid && await window.BkVisionSDK.init(`#dashboard-${this.renderId}`, this.uid, this.dataInfo)
+ console.log(this.visionApp, 'after init')
} else {
console.error('sdk 加载异常')
}
diff --git a/lib/client/src/components/render/pc/render-component.js b/lib/client/src/components/render/pc/render-component.js
index 473b1f8a5..5eea49a35 100644
--- a/lib/client/src/components/render/pc/render-component.js
+++ b/lib/client/src/components/render/pc/render-component.js
@@ -16,6 +16,7 @@ import WidgetFormItem from './widget/form/form-item'
import WidgetMdEditor from './widget/md-editor/md-editor'
import WidgetBkTable from './widget/table/table'
import WidgetVanPicker from './widget/van-picker'
+import widgetBkVision from './widget/bk-vision/bk-vision'
import WidgetFormContainer from './widget/form-container'
import WidgetDataManageContainer from './widget/data-manage-container/form-data-manage/edit/index'
import WidgetFlowManageContainer from './widget/flow-manage-container/edit/index'
@@ -51,6 +52,7 @@ export default {
WidgetMdEditor,
WidgetBkTable,
WidgetVanPicker,
+ widgetBkVision,
WidgetFormContainer,
WidgetDataManageContainer,
WidgetFlowManageContainer,
diff --git a/lib/client/src/components/render/pc/widget/bk-vision/bk-vision.js b/lib/client/src/components/render/pc/widget/bk-vision/bk-vision.js
new file mode 100644
index 000000000..c3a1810ba
--- /dev/null
+++ b/lib/client/src/components/render/pc/widget/bk-vision/bk-vision.js
@@ -0,0 +1,138 @@
+/**
+ * Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community Edition) available.
+ * Copyright (C) 2025 Tencent. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * 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.
+ */
+
+import { h } from 'bk-lesscode-render'
+import LC from '@/element-materials/core'
+import { uuid, debounce } from 'shared/util'
+
+export default {
+ name: 'widget-bk-vision',
+ inheritAttrs: false,
+ props: {
+ componentData: {
+ type: Object
+ },
+ uid: {
+ type: String
+ }
+ },
+ data () {
+ return {
+ visionApp: {},
+ renderId: '',
+ apiPrefix: '/api/bkvision/',
+ cdnPrefix: 'https://staticfile.qq.com/bkvision/pbb9b207ba200407982a9bd3d3f2895d4/latest/'
+ }
+ },
+ computed: {
+ compUid () {
+ return this.uid || this.componentData?.prop?.uid
+ },
+ dataInfo () {
+ return {
+ apiPrefix: '/api/bkvision/'
+ // waterMark: { content: this.componentData?.watchMark || 'bk-lesscode' }
+ }
+ }
+ },
+ watch: {
+ compUid (val) {
+ console.log('compUid change', val)
+ this.debounceInit()
+ }
+ },
+ created () {
+ this.renderId = uuid(6)
+ LC.addEventListener('update', this.updateCallback)
+ },
+ mounted () {
+ this.debounceInit = debounce(this.initPanel)
+ if (!window.BkVisionSDK) {
+ console.log('load sdk')
+ this.loadSdk()
+ } else {
+ console.log('bkvision sdk exist')
+ this.initPanel()
+ }
+ },
+ beforeDestroy () {
+ LC.removeEventListener('update', this.updateCallback)
+ },
+ methods: {
+ updateCallback ({ target }) {
+ if (target.componentId === this.componentData?.componentId) {
+ this.$forceUpdate()
+ this.debounceInit()
+ }
+ },
+ async loadSdk () {
+ const link = document.createElement('link')
+ link.href = 'https://staticfile.qq.com/bkvision/pbb9b207ba200407982a9bd3d3f2895d4/3c3de519287048dcb4c5a03d47ebf33f/main.css'
+ link.rel = 'stylesheet'
+ document.body.append(link)
+ await this.loadScript('main.js')
+ this.initPanel()
+ },
+ loadScript (file) {
+ return new Promise((resolve, reject) => {
+ const url = this.cdnPrefix + file
+ const script = document.createElement('script')
+ script.src = url
+ document.body.append(script)
+ script.onload = () => {
+ resolve()
+ }
+ })
+ },
+ async initPanel () {
+ const compUid = this.compUid
+ console.log('init panel', compUid)
+ if (window.BkVisionSDK) {
+ try {
+ if (this.visionApp && Object.keys(this.visionApp).length) {
+ this.visionApp?.unmount()
+ }
+ } catch (error) {
+ console.error(error?.message || error, 'unmount bk-vision error')
+ }
+
+ this.visionApp = compUid && await window.BkVisionSDK.init(`.dashboard-${this.renderId}`, compUid, this.dataInfo)
+ console.log(this.visionApp, 'after init', compUid)
+ } else {
+ console.error('sdk 加载异常')
+ }
+ }
+ },
+ render (render) {
+ h.init(render)
+
+ const self = this
+
+ return h({
+ component: 'div',
+ class: 'lesscode-bk-vision-container',
+ children: [
+ !self.compUid ? h({
+ component: 'bk-exception',
+ class: 'exception-wrap-item exception-part exception-gray',
+ props: {
+ type: '404',
+ scene: 'part'
+ }
+ })
+ : h({
+ component: 'div',
+ class: `dashboard-${self.renderId}`
+ })
+ ]
+ })
+ }
+}
diff --git a/lib/client/src/element-materials/core/extends/merge-render-events.js b/lib/client/src/element-materials/core/extends/merge-render-events.js
index efd746517..6d8b7b175 100644
--- a/lib/client/src/element-materials/core/extends/merge-render-events.js
+++ b/lib/client/src/element-materials/core/extends/merge-render-events.js
@@ -1,4 +1,4 @@
-import _ from 'lodash'
+import { safeObjectMerge } from '../../../../../shared/security/property-injection-guard'
/**
* @desc 设置节点的 renderEvents(增量添加)
@@ -13,13 +13,8 @@ export default function (node, events) {
if (!isObject(events)) {
throw new Error(window.i18n.t('设置 mergeRenderEvents 值只支持 Object'))
}
- const customizer = (a, b) => {
- if (isObject(a) && isObject(b)) {
- return _.mergeWith(a, b, customizer)
- } else {
- return b
- }
- }
- node.renderEvents = _.mergeWith(node.renderEvents, events, customizer)
+
+ // 使用安全的对象合并方式,防止属性注入攻击
+ node.renderEvents = safeObjectMerge(node.renderEvents || {}, events, { deep: true })
return true
}
diff --git a/lib/client/src/element-materials/core/extends/set-prop.js b/lib/client/src/element-materials/core/extends/set-prop.js
index 9d9a7422c..bf97dad69 100644
--- a/lib/client/src/element-materials/core/extends/set-prop.js
+++ b/lib/client/src/element-materials/core/extends/set-prop.js
@@ -1,4 +1,6 @@
import _ from 'lodash'
+import { safeForEachProperty, isSafePropertyKey } from '../../../../../shared/security/property-injection-guard'
+
/**
* @desc 设置节点 renderProps 上指定 prop 的值
* @param { Node } node
@@ -12,27 +14,33 @@ import _ from 'lodash'
export default function (node, params1, params2) {
let propData = {}
if (Object.prototype.toString.call(params1) === '[object String]') {
+ // 验证属性名是否安全
+ if (!isSafePropertyKey(params1)) {
+ console.warn(`[Security] Rejected unsafe property name: ${params1}`)
+ return false
+ }
propData[params1] = params2
} else {
propData = params1
}
const materialProps = node.material.props
- Object.keys(propData).forEach(propName => {
+ // 使用安全的方式遍历属性,防止原型链污染
+ safeForEachProperty(propData, (propName, propValue) => {
if (materialProps.hasOwnProperty(propName)) {
const propConfigType = materialProps[propName].type
- node.renderProps[propName] = _.cloneDeep(propData[propName])
+ node.renderProps[propName] = _.cloneDeep(propValue)
// 没传 valueType,
- if (!propData[propName].valueType) {
+ if (!propValue.valueType) {
// 从组件配置中自动推导出默认 valueType (如果是数组默认取第一个)
const valueType = Array.isArray(propConfigType)
? propConfigType[0]
: propConfigType
node.renderProps[propName].valueType = valueType
}
- if (!propData[propName].payload) {
+ if (!propValue.payload) {
node.renderProps[propName].payload = {}
}
}
diff --git a/lib/client/src/element-materials/core/helper/merge-data-to-node.js b/lib/client/src/element-materials/core/helper/merge-data-to-node.js
index 096423aa8..38a06c8dd 100644
--- a/lib/client/src/element-materials/core/helper/merge-data-to-node.js
+++ b/lib/client/src/element-materials/core/helper/merge-data-to-node.js
@@ -1,23 +1,29 @@
+import { safeObjectMerge, safeForEachProperty } from '../../../../../shared/security/property-injection-guard'
+
export default function (data, node) {
if (data.renderStyles) {
- node.renderStyles = data.renderStyles
+ // 安全地合并样式对象,防止属性注入攻击
+ node.renderStyles = safeObjectMerge(node.renderStyles || {}, data.renderStyles, { deep: true })
}
if (data.renderProps) {
// prop 通过 merge 的方式加载
// 兼容组件 prop 扩展的场景
- Object.keys(data.renderProps).forEach(key => {
- node.renderProps[key] = data.renderProps[key]
+ // 使用安全的方式遍历和设置属性,防止原型链污染
+ safeForEachProperty(data.renderProps, (key, value) => {
+ node.renderProps[key] = value
})
}
if (data.renderDirectives) {
node.renderDirectives = data.renderDirectives
}
if (data.renderEvents) {
- node.renderEvents = data.renderEvents
+ // 安全地合并事件对象
+ node.renderEvents = safeObjectMerge(node.renderEvents || {}, data.renderEvents, { deep: true })
}
if (data.renderAlign) {
- Object.keys(data.renderAlign).forEach(key => {
- node.renderAlign[key] = data.renderAlign[key]
+ // 安全地合并对齐属性
+ safeForEachProperty(data.renderAlign, (key, value) => {
+ node.renderAlign[key] = value
})
}
if (data.renderPerms) {
diff --git a/lib/client/src/element-materials/materials/vue2/bk/bk-vision/index.js b/lib/client/src/element-materials/materials/vue2/bk/bk-vision/index.js
index cb3e921b4..82151e3ac 100644
--- a/lib/client/src/element-materials/materials/vue2/bk/bk-vision/index.js
+++ b/lib/client/src/element-materials/materials/vue2/bk/bk-vision/index.js
@@ -13,7 +13,7 @@ export default {
name: 'bkvision',
type: 'widget-bk-vision',
display: 'none',
- displayName: '蓝鲸图表',
+ displayName: 'BKVision仪表盘',
icon: 'bk-drag-histogram',
group: 'ECharts',
order: 1,
diff --git a/lib/client/src/element-materials/materials/vue2/bk/paragraph/index.js b/lib/client/src/element-materials/materials/vue2/bk/paragraph/index.js
index ab7082c1f..37f03b613 100644
--- a/lib/client/src/element-materials/materials/vue2/bk/paragraph/index.js
+++ b/lib/client/src/element-materials/materials/vue2/bk/paragraph/index.js
@@ -28,15 +28,15 @@ export default {
verticalAlign: 'middle'
},
directives: [
- {
- type: 'v-html',
- prop: 'slots',
- format: 'variable',
- valueTypeInclude: ['string'],
- tips () {
- return '内容直接作为普通 HTML 插入,Vue 模板语法不会被解析'
- }
- }
+ // {
+ // type: 'v-html',
+ // prop: 'slots',
+ // format: 'variable',
+ // valueTypeInclude: ['string'],
+ // tips () {
+ // return '内容直接作为普通 HTML 插入,Vue 模板语法不会被解析'
+ // }
+ // }
],
groups: [
{ label: 'hover时提示', value: 'tooltip' },
diff --git a/lib/client/src/element-materials/materials/vue2/vant/paragraph/index.js b/lib/client/src/element-materials/materials/vue2/vant/paragraph/index.js
index 633c62bb7..f2c071a59 100644
--- a/lib/client/src/element-materials/materials/vue2/vant/paragraph/index.js
+++ b/lib/client/src/element-materials/materials/vue2/vant/paragraph/index.js
@@ -27,15 +27,15 @@ export default {
wordBreak: 'break-all'
},
directives: [
- {
- type: 'v-html',
- prop: 'slots',
- format: 'variable',
- valueTypeInclude: ['string'],
- tips () {
- return '内容直接作为普通 HTML 插入,Vue 模板语法不会被解析'
- }
- }
+ // {
+ // type: 'v-html',
+ // prop: 'slots',
+ // format: 'variable',
+ // valueTypeInclude: ['string'],
+ // tips () {
+ // return '内容直接作为普通 HTML 插入,Vue 模板语法不会被解析'
+ // }
+ // }
],
groups: [
{ label: 'hover时提示', value: 'tooltip' },
diff --git a/lib/client/src/element-materials/materials/vue3/bk/bk-vision/index.js b/lib/client/src/element-materials/materials/vue3/bk/bk-vision/index.js
new file mode 100644
index 000000000..95ae77bc1
--- /dev/null
+++ b/lib/client/src/element-materials/materials/vue3/bk/bk-vision/index.js
@@ -0,0 +1,47 @@
+/**
+ * Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community Edition) available.
+ * Copyright (C) 2025 Tencent. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * 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.
+ */
+
+export default {
+ name: 'bkvision',
+ type: 'widget-bk-vision',
+ display: 'none',
+ displayName: 'BKVision仪表盘',
+ icon: 'bk-drag-histogram',
+ group: 'ECharts',
+ order: 1,
+ events: [],
+ styles: [
+ {
+ name: 'size',
+ include: ['width', 'height']
+ }
+ ],
+ renderStyles: {
+ width: '100%',
+ height: '100%',
+ minHeight: '100px'
+ },
+ props: {
+ uid: {
+ type: 'request-select',
+ payload: {
+ url: '/bkvision/shareList',
+ key: 'uid',
+ value: 'name',
+ slotText: '新增仪表盘分享',
+ slotLink: process.env.BK_VISION_WEB_URL + '/#/space',
+ isGroup: true,
+ groupChildren: 'share'
+ },
+ tips: '图表平台嵌入管理中的仪表盘分享id, 请先到图表平台配置,步骤:
1、选择想要分享的空间或新建空间, 进入空间后新建仪表盘
2、点击上方嵌入管理、新增嵌入
3、新增时选择想要分享的仪表盘、蓝鲸应用ID选择“当前应用”,并填入“visual-layout”(运维开发平台的应用ID)
4、下一步嵌入模式选择“JS-SDK嵌入”, 提交后返回列表页开启发布分享
5、回到LessCode平台选择所需的分享'
+ }
+ }
+}
diff --git a/lib/client/src/element-materials/materials/vue3/bk/index.js b/lib/client/src/element-materials/materials/vue3/bk/index.js
index a25d4fbde..7c4798ef2 100644
--- a/lib/client/src/element-materials/materials/vue3/bk/index.js
+++ b/lib/client/src/element-materials/materials/vue3/bk/index.js
@@ -72,6 +72,7 @@ import chartsPie from './charts-pie'
import echartsLine from './echarts-line'
import echartsBar from './echarts-bar'
import echartsPie from './echarts-pie'
+import bkVision from './bk-vision'
import bkChartsLine from './bk-charts-line'
import bkChartsBar from './bk-charts-bar'
import bkChartsPie from './bk-charts-pie'
@@ -144,6 +145,7 @@ const bkComponents = Object.seal([
echartsLine,
echartsBar,
echartsPie,
+ bkVision,
bkChartsLine,
bkChartsBar,
bkChartsPie,
diff --git a/lib/client/src/element-materials/materials/vue3/bk/paragraph/index.js b/lib/client/src/element-materials/materials/vue3/bk/paragraph/index.js
index 372997c6b..369be1811 100644
--- a/lib/client/src/element-materials/materials/vue3/bk/paragraph/index.js
+++ b/lib/client/src/element-materials/materials/vue3/bk/paragraph/index.js
@@ -28,15 +28,15 @@ export default {
verticalAlign: 'middle'
},
directives: [
- {
- type: 'v-html',
- prop: 'slots',
- format: 'variable',
- valueTypeInclude: ['string'],
- tips () {
- return '内容直接作为普通 HTML 插入,Vue 模板语法不会被解析'
- }
- }
+ // {
+ // type: 'v-html',
+ // prop: 'slots',
+ // format: 'variable',
+ // valueTypeInclude: ['string'],
+ // tips () {
+ // return '内容直接作为普通 HTML 插入,Vue 模板语法不会被解析'
+ // }
+ // }
],
groups: [
{ label: 'hover时提示', value: 'tooltip' },
diff --git a/lib/client/src/element-materials/materials/vue3/vant/paragraph/index.js b/lib/client/src/element-materials/materials/vue3/vant/paragraph/index.js
index 50e0dd6c8..1677ace7a 100644
--- a/lib/client/src/element-materials/materials/vue3/vant/paragraph/index.js
+++ b/lib/client/src/element-materials/materials/vue3/vant/paragraph/index.js
@@ -27,15 +27,15 @@ export default {
wordBreak: 'break-all'
},
directives: [
- {
- type: 'v-html',
- prop: 'slots',
- format: 'variable',
- valueTypeInclude: ['string'],
- tips () {
- return '内容直接作为普通 HTML 插入,Vue 模板语法不会被解析'
- }
- }
+ // {
+ // type: 'v-html',
+ // prop: 'slots',
+ // format: 'variable',
+ // valueTypeInclude: ['string'],
+ // tips () {
+ // return '内容直接作为普通 HTML 插入,Vue 模板语法不会被解析'
+ // }
+ // }
],
groups: [
{ label: 'hover时提示', value: 'tooltip' },
diff --git a/lib/client/src/element-materials/modifier/component/props/components/render-prop.vue b/lib/client/src/element-materials/modifier/component/props/components/render-prop.vue
index fde9fccd0..f7990018c 100644
--- a/lib/client/src/element-materials/modifier/component/props/components/render-prop.vue
+++ b/lib/client/src/element-materials/modifier/component/props/components/render-prop.vue
@@ -23,7 +23,7 @@
+ v-bk-xss-html="displayName">
@@ -110,7 +110,7 @@
import _ from 'lodash'
import { mapActions } from 'vuex'
import LC from '@/element-materials/core'
- import DOMPurify from 'dompurify'
+ import { filterXss } from '@blueking/xss-filter'
import { camelCase, camelCaseTransformMerge } from 'change-case'
import { transformTipsWidth } from '@/common/util'
import safeStringify from '@/common/json-safe-stringify'
@@ -375,7 +375,7 @@
name = `${this.describe.displayName}(${this.name})`
}
}
- return DOMPurify.sanitize(name)
+ return name
},
/**
* @desc 不支持的变量切换类型(variable、expression)
@@ -392,7 +392,7 @@
return {
placements: ['left-start'],
maxWidth: 300,
- content: DOMPurify.sanitize(this.tipsContent)
+ content: filterXss(this.tipsContent)
}
},
tipsContent () {
diff --git a/lib/client/src/element-materials/modifier/component/styles/strategy/custom-style.vue b/lib/client/src/element-materials/modifier/component/styles/strategy/custom-style.vue
index 91ce13549..74dd2f7a1 100644
--- a/lib/client/src/element-materials/modifier/component/styles/strategy/custom-style.vue
+++ b/lib/client/src/element-materials/modifier/component/styles/strategy/custom-style.vue
@@ -67,6 +67,7 @@
import StyleItem from '../layout/item'
import Monaco from '@/components/monaco'
import { camelCase } from 'change-case'
+ import { isSafePropertyKey } from '../../../../../../../shared/security/property-injection-guard'
export default {
components: {
@@ -143,7 +144,11 @@
if (item) {
const itemArr = item.split(':')
if (itemArr.length === 2 && itemArr[0].trim() && itemArr[1].trim()) {
- Object.assign(customMap, { [itemArr[0].trim()]: itemArr[1].trim() })
+ const cssProperty = itemArr[0].trim()
+ // 验证CSS属性名是否安全,防止属性注入攻击
+ if (isSafePropertyKey(cssProperty)) {
+ customMap[cssProperty] = itemArr[1].trim()
+ }
}
}
})
diff --git a/lib/client/src/locales/lang/en.json b/lib/client/src/locales/lang/en.json
index f59a6c2d0..1c231e65f 100644
--- a/lib/client/src/locales/lang/en.json
+++ b/lib/client/src/locales/lang/en.json
@@ -2493,7 +2493,7 @@
"边框宽度": "Border Width",
"边框颜色": "Border Color",
"饼图": "Pie Chart",
- "蓝鲸图表": "BKVision",
+ "BKVision仪表盘": "BKVision",
"鼠标Hover时,图块偏移的距离": "When the mouse hovers, the distance of the block offset",
"数据点Hover时的背景色": "The background color of data point hover",
"数据点的边框颜色": "The border color of the data point",
diff --git a/lib/client/src/locales/lang/zh-cn.json b/lib/client/src/locales/lang/zh-cn.json
index e34506239..3fed75259 100644
--- a/lib/client/src/locales/lang/zh-cn.json
+++ b/lib/client/src/locales/lang/zh-cn.json
@@ -2404,7 +2404,7 @@
"鼠标Hover时,图块偏移的距离": "鼠标Hover时,图块偏移的距离",
"边框宽度": "边框宽度",
"边框颜色": "边框颜色",
- "蓝鲸图表": "蓝鲸图表",
+ "BKVision仪表盘": "BKVision仪表盘",
"流程": "流程",
"饼图": "饼图",
"数据点Hover时的背景色": "数据点Hover时的背景色",
diff --git a/lib/client/src/main.js b/lib/client/src/main.js
index 8e16f7434..a65cdae1d 100644
--- a/lib/client/src/main.js
+++ b/lib/client/src/main.js
@@ -44,6 +44,7 @@ import pureAxios from '@/api/pureAxios.js'
import MgContentLoader from '@/components/loader'
import './directives'
+import { BkXssFilterDirective } from '@blueking/xss-filter'
import { IAM_ACTION } from 'shared/constant'
import './bk-icon/style.css'
@@ -55,6 +56,7 @@ Vue.prototype.$td = targetData
Vue.prototype.$IAM_ACTION = IAM_ACTION
Vue.use(mavonEditor)
+Vue.use(BkXssFilterDirective)
Vue.component('VueDraggable', VueDraggable)
Vue.component('app-exception', Exception)
diff --git a/lib/client/src/preview/component.js b/lib/client/src/preview/component.js
index b97e1a704..69828b2c8 100644
--- a/lib/client/src/preview/component.js
+++ b/lib/client/src/preview/component.js
@@ -23,6 +23,7 @@ import widgetTableColumn from '@/components/render/pc/widget/table/table-column'
import widgetVanPicker from '@/components/render/pc/widget/van-picker'
import bkCharts from '@/components/render/pc/widget/bk-charts/bk-charts'
import chart from '@/components/render/pc/widget/chart/chart'
+import widgetBkVision from '@/components/render/pc/widget/bk-vision/bk-vision'
import WidgetFormContainer from '@/form-engine/renderer/index'
import WidgetDataManageContainer from '@/components/render/pc/widget/data-manage-container/form-data-manage/preview'
import WidgetFlowManageContainer from '@/components/render/pc/widget/flow-manage-container/preview'
@@ -83,6 +84,7 @@ const registerSysComponents = () => {
registerComponent('chart', chart)
// 新echarts配置
registerComponent('echarts', chart)
+ registerComponent('widget-bk-vision', widgetBkVision)
registerComponent('widget-van-picker', widgetVanPicker)
registerComponent('widget-form-container', WidgetFormContainer)
registerComponent('widget-data-manage-container', WidgetDataManageContainer)
diff --git a/lib/client/src/preview/overlay.js b/lib/client/src/preview/overlay.js
index d0ce42b34..ddf019135 100644
--- a/lib/client/src/preview/overlay.js
+++ b/lib/client/src/preview/overlay.js
@@ -11,14 +11,14 @@
import Vue from 'vue'
import Img403 from '@/images/403.png'
import ApplyPage from '@/components/apply-permission/apply-page.vue'
-import DOMPurify from 'dompurify'
+import { filterXss } from '@blueking/xss-filter'
export const handleError = (err = {}) => {
console.error(err)
const response = err.response || {}
const data = response.data || {}
let message = data.message || err.message || window.i18n.t('无法连接到后端服务,请稍候再试。')
- message = DOMPurify.sanitize(message)
+ message = filterXss(message)
const divStyle = `
text-align: center;
width: 400px;
diff --git a/lib/client/src/views/project/function-manage/children/list.vue b/lib/client/src/views/project/function-manage/children/list.vue
index 6b0fb9cb3..37534b1ac 100644
--- a/lib/client/src/views/project/function-manage/children/list.vue
+++ b/lib/client/src/views/project/function-manage/children/list.vue
@@ -168,7 +168,7 @@
computedFunctionList () {
const searchReg = new RegExp(this.searchFunStr, 'i')
- return this.functionList.filter((func) => searchReg.test(func.funcName))
+ return this.functionList.filter((func) => searchReg.test(func.funcName) || searchReg.test(func.funcCode))
},
emptyType () {
if (this.searchFunStr.length > 0) {
diff --git a/lib/client/src/views/project/page-manage/children/page-menu-item.vue b/lib/client/src/views/project/page-manage/children/page-menu-item.vue
index b6cc9a7a5..9c5129546 100644
--- a/lib/client/src/views/project/page-manage/children/page-menu-item.vue
+++ b/lib/client/src/views/project/page-manage/children/page-menu-item.vue
@@ -30,6 +30,7 @@
-
diff --git a/lib/server/project-template/vue3/project-init-code/lib/server/controller/bkvision.js b/lib/server/project-template/vue3/project-init-code/lib/server/controller/bkvision.js
index 53a420f9d..a67aeefbb 100644
--- a/lib/server/project-template/vue3/project-init-code/lib/server/controller/bkvision.js
+++ b/lib/server/project-template/vue3/project-init-code/lib/server/controller/bkvision.js
@@ -42,7 +42,7 @@ export default class bkVisionController {
ctx.body = res.data
}
- @All('/api/v1/(variable|datasource)/*')
+ @All('/api/v1/(variable|datasource|dataset)/*')
async proxyPostApi (
@Ctx() ctx,
@Ctx({ name: 'captures' }) captures,
diff --git a/lib/server/project-template/vue3/project-init-code/lib/server/controller/mock-data.js b/lib/server/project-template/vue3/project-init-code/lib/server/controller/mock-data.js
index be682c291..c60c5bfb9 100644
--- a/lib/server/project-template/vue3/project-init-code/lib/server/controller/mock-data.js
+++ b/lib/server/project-template/vue3/project-init-code/lib/server/controller/mock-data.js
@@ -6,6 +6,7 @@ import {
Ctx,
OutputJson
} from '../decorator'
+import { validateUrl } from '../util/validate-url'
const METHODS_WITH_DATA = ['post', 'put', 'patch']
@@ -15,7 +16,6 @@ export default class DataController {
@Post('/getApiData')
async getApiData (@Ctx() ctx) {
const {
- url,
type = 'get',
withToken = 0,
apiData = {},
@@ -29,14 +29,32 @@ export default class DataController {
hostUrl = ctx.origin + process.env.BKPAAS_SUB_PATH
}
+ let url = ctx.request.body?.url
+ url = url?.replaceAll('@', '')
+ // /开头的是相对路径
+ if (url.startsWith('/')) {
+ url = hostUrl + url
+ }
+
+ const checkUrl = await validateUrl(url, ctx.origin, process.env.BK_API_DOMAIN_WHITE_LIST)
+
+ if (checkUrl?.result !== true) {
+ ctx.throwError({
+ message: checkUrl?.message || '请求异常'
+ })
+ }
+
// 发送请求的参数
const httpConf = {
...axiosConfig,
- url: /^http(s)?\:\/\//.test(url) ? url : hostUrl + url,
+ url,
method: type,
headers: {
...axiosConfig?.headers
- }
+ },
+ // 不允许重定向
+ maxRedirects: 0,
+ timeout: 5000
}
// 请求携带的数据
diff --git a/lib/server/project-template/vue3/project-init-code/lib/server/service/data-service.js b/lib/server/project-template/vue3/project-init-code/lib/server/service/data-service.js
index b4454e432..c2f2a1aa3 100644
--- a/lib/server/project-template/vue3/project-init-code/lib/server/service/data-service.js
+++ b/lib/server/project-template/vue3/project-init-code/lib/server/service/data-service.js
@@ -241,7 +241,7 @@ export function getDataService (name = 'default', customEntityMap) {
*/
count (tableFileName, query = { deleteFlag: 0 }) {
const repository = getRepositoryByName(tableFileName)
- return repository.count(transformQuery(query))
+ return repository.count({ where: transformQuery(query) })
},
/**
* 获取数据详情
@@ -251,7 +251,7 @@ export function getDataService (name = 'default', customEntityMap) {
*/
findOne (tableFileName, query = {}) {
const repository = getRepositoryByName(tableFileName)
- return repository.findOne(transformQuery(query)) || {}
+ return repository.findOne({ where: transformQuery(query) }) || {}
},
/**
@@ -279,7 +279,7 @@ export function getDataService (name = 'default', customEntityMap) {
* @returns 新增结果
*/
async bulkAdd (tableFileName, dataList) {
- dataList.forEach(item =>{
+ dataList.forEach(item => {
item.id = null
})
const repository = getRepositoryByName(tableFileName)
@@ -303,7 +303,7 @@ export function getDataService (name = 'default', customEntityMap) {
const repository = getRepositoryByName(tableFileName)
try {
const { id, ...rest } = data
- const { affected = 0 } = await repository.update(transformQuery(where), transformData(rest, repository.metadata.columns))
+ const { affected = 0 } = await repository.update({ where: transformQuery(where) }, transformData(rest, repository.metadata.columns))
return affected
} catch (error) {
throw new Error(error.sqlMessage || error)
@@ -327,7 +327,7 @@ export function getDataService (name = 'default', customEntityMap) {
Object.assign(editData, data)
try {
- const { affected = 0 } = await repository.update(data.id, transformData(editData, repository.metadata.columns))
+ const { affected = 0 } = await repository.update({ where: { id: data.id } }, transformData(editData, repository.metadata.columns))
return affected
} catch (error) {
throw new Error(error.sqlMessage || error)
@@ -356,7 +356,7 @@ export function getDataService (name = 'default', customEntityMap) {
try {
const transformList = transformData(editDataList, repository.metadata.columns)
- const updateResult = await Promise.all(transformList.map((data) => repository.update(data.id, data)))
+ const updateResult = await Promise.all(transformList.map((data) => repository.update({ where: { id: data.id } }, data)))
return updateResult.reduce((acc, { affected }) => {
acc += affected
return acc
@@ -384,7 +384,9 @@ export function getDataService (name = 'default', customEntityMap) {
throw new Error(global.i18n.t('删除 {{n}} 表数据的时候,需要使用 unique 或者 id 字段保证删除数据唯一', { n: tableFileName }))
}
try {
- const { affected = 0 } = await repository.delete(data)
+ // TypeORM 0.3+ 需要将条件放在 { where: ... } 中
+ const deleteCondition = typeof data === 'object' ? { where: data } : { where: { id: data } }
+ const { affected = 0 } = await repository.delete(deleteCondition)
return affected
} catch (error) {
throw new Error(error.sqlMessage || error)
@@ -401,7 +403,7 @@ export function getDataService (name = 'default', customEntityMap) {
if ([undefined, null].includes(ids)) throw new Error(`批量删除 ${tableFileName} 表数据的时候,需要传入 ids 参数`)
const repository = getRepositoryByName(tableFileName)
try {
- const { affected = 0 } = await repository.delete(ids)
+ const { affected = 0 } = await repository.delete({ where: { id: In(ids) } })
return affected
} catch (error) {
throw new Error(error.sqlMessage || error)
diff --git a/lib/server/project-template/vue3/project-init-code/lib/server/util/validate-url.js b/lib/server/project-template/vue3/project-init-code/lib/server/util/validate-url.js
new file mode 100644
index 000000000..9ba917b94
--- /dev/null
+++ b/lib/server/project-template/vue3/project-init-code/lib/server/util/validate-url.js
@@ -0,0 +1,124 @@
+const dns = require('dns').promises
+const ipaddr = require('ipaddr.js')
+
+function isPrivateIP (ip) {
+ if (ip === '127.0.0.1' && process.env.NODE_ENV === 'development') {
+ return false
+ }
+ let addr
+ try {
+ addr = ipaddr.parse(ip)
+ } catch (e) {
+ return true
+ }
+
+ if (addr.kind() === 'ipv4') {
+ const range = addr.range()
+ return (
+ range === 'private'
+ || range === 'loopback'
+ || range === 'linkLocal'
+ || range === 'broadcast'
+ || range === 'carrierGradeNat'
+ || range === 'unspecified'
+ )
+ } else if (addr.kind() === 'ipv6') {
+ return (
+ addr.isLoopback()
+ || addr.range() === 'uniqueLocal'
+ || addr.range() === 'linkLocal'
+ )
+ }
+ return false
+}
+
+function checkPort (port) {
+ port = parseInt(port)
+ // 禁止端口
+ const forbiddenPort = [21, 22, 23, 25, 69, 135, 137, 138, 139,
+ 161, 162, 389, 465, 514, 587, 636, 873, 1099, 2181, 2375,
+ 2376, 27017, 3306, 3389, 36000, 4848, 50070, 50075, 5432,
+ 56000, 5900, 5901, 6379, 7001, 7002, 9200, 9300, 10050, 10051,
+ 10250, 10255, 11211]
+ return forbiddenPort.includes(port)
+}
+
+// 获取当前环境的主域名
+function getMainDomain (origin) {
+ try {
+ const url = new URL(origin)
+ const hostnameParts = url?.hostname?.split('.')
+ const partsLength = hostnameParts?.length
+
+ // 如果域名部分少于2个,返回null
+ if (partsLength < 2) {
+ return null
+ }
+
+ // 提取主域名
+ const mainDomain = hostnameParts.slice(partsLength - 2).join('.')
+ return `.${mainDomain}`
+ } catch (error) {
+ console.error('Invalid URL:', error)
+ return null
+ }
+}
+
+export const validateUrl = async (userInputUrl, originalUrl, whiteList = []) => {
+ let parsedUrl = ''
+ try {
+ parsedUrl = new URL(userInputUrl)
+ } catch (err) {
+ return {
+ result: false,
+ message: 'url异常'
+ }
+ }
+
+ // 只允许 http 和 https 协议
+ if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
+ return {
+ result: false,
+ message: '只允许http跟https协议'
+ }
+ }
+
+ // 域名必须在白名单
+ const domain = getMainDomain(originalUrl)
+ if (!domain) {
+ return {
+ result: false,
+ message: '无法解析需要访问的域名'
+ }
+ }
+ const targetDomain = getMainDomain(userInputUrl)
+ if (!parsedUrl.hostname?.endsWith(domain) && !whiteList?.includes(targetDomain)) {
+ return {
+ result: false,
+ message: '只允许访问跟当前环境同域的域名'
+ }
+ }
+
+ const port = parsedUrl.port
+ if (port && checkPort(port)) {
+ return {
+ result: false,
+ message: '禁止使用此端口'
+ }
+ }
+ // 解析域名对应的 IP 地址,防止 DNS 解析绕过
+ const addresses = await dns.lookup(parsedUrl.hostname, { all: true })
+ for (const addr of addresses) {
+ if (isPrivateIP(addr.address)) {
+ return {
+ result: false,
+ message: '禁止访问私有ip'
+ }
+ }
+ }
+
+ return {
+ result: true,
+ message: ''
+ }
+}
diff --git a/lib/server/project-template/vue3/project-init-code/package.json b/lib/server/project-template/vue3/project-init-code/package.json
index cd994d7ea..82a726eb7 100644
--- a/lib/server/project-template/vue3/project-init-code/package.json
+++ b/lib/server/project-template/vue3/project-init-code/package.json
@@ -108,7 +108,7 @@
"acorn": "~7.2.0",
"ansi_up": "^5.0.0",
"app-root-path": "~3.0.0",
- "axios": "~1.6.0",
+ "axios": "~1.12.0",
"babel-eslint": "~10.0.3",
"babel-plugin-parameter-decorator": "^1.0.16",
"better-npm-run": "~0.1.1",
@@ -154,7 +154,7 @@
"node-request-context": "~1.0.5",
"nodemon": "~1.19.3",
"ora": "~4.0.2",
- "path-to-regexp": "^6.1.0",
+ "path-to-regexp": "^8.3.0",
"postcss": "~8.4.31",
"postcss-import": "^15.0.0",
"postcss-mixins": "^9.0.4",
@@ -169,7 +169,7 @@
"shx": "~0.3.2",
"swig": "~1.4.2",
"mavon-editor": "^2.9.0",
- "typeorm": "^0.2.25",
+ "typeorm": "~0.3.27",
"typescript": "^4.4.3",
"vue": "~3.2.41",
"vue-echarts": "^5.0.0-beta.0",
diff --git a/lib/server/router/component.js b/lib/server/router/component.js
index 53b1963dc..91e2356f8 100644
--- a/lib/server/router/component.js
+++ b/lib/server/router/component.js
@@ -36,13 +36,13 @@ const router = new Router({
})
// 只校验projectId
-router.use(['/list', '/export', '/upload'], async (ctx, next) => {
+router.use(['/list', '/export', '/upload', '/useing'], async (ctx, next) => {
const projectId = ctx.query.belongProjectId
await handleProjectPerm(ctx, next, projectId)
})
// 只校验projectId
-router.use(['/using', '/create', '/version-detail'], async (ctx, next) => {
+router.use(['/create', '/version-detail'], async (ctx, next) => {
await handleProjectPerm(ctx, next)
})
diff --git a/lib/server/router/open-api.js b/lib/server/router/open-api.js
index c2ef90541..7d07cd844 100644
--- a/lib/server/router/open-api.js
+++ b/lib/server/router/open-api.js
@@ -57,7 +57,7 @@ router.use('*', async (ctx, next) => {
app: {},
user
}
- const userData = await getRepository(User).findOne({ username: user.username })
+ const userData = await getRepository(User).findOne({ where: { username: user.username } })
let userId = userData?.id
if (!userId) {
userId = await addUser({
diff --git a/lib/server/router/page.js b/lib/server/router/page.js
index 8aa79f35a..96ca1b5ea 100644
--- a/lib/server/router/page.js
+++ b/lib/server/router/page.js
@@ -58,7 +58,7 @@ router.use(['/create'], async (ctx, next) => {
}
})
-const hasPageIdRoutes = ['/update', '/copy', '/delete', '/detail', '/updatePageActive', '/occupyPage']
+const hasPageIdRoutes = ['/update', '/copy', '/delete', '/detail', '/updatePageActive', '/occupyPage', '/releasePage']
router.use(hasPageIdRoutes, async (ctx, next) => {
const tableName = 'PROJECT_PAGE'
const resourceKey = 'pageId'
diff --git a/lib/server/service/business/open-api.js b/lib/server/service/business/open-api.js
index 676467b24..355d05bb5 100644
--- a/lib/server/service/business/open-api.js
+++ b/lib/server/service/business/open-api.js
@@ -102,7 +102,7 @@ export const generateApiGateway = async () => {
if (transformVersionToNum(version) < transformVersionToNum(openApiJson.info.version)) {
try {
const formData = new FormData()
- formData.append('file', fs.createReadStream(path.resolve(__dirname, './system-conf/apigw-docs.zip')))
+ formData.append('file', fs.createReadStream(path.resolve(__dirname, '../../system-conf/apigw-docs.zip')))
// 更新资源文档
await execApiGateWay({
apiName: 'bk-apigateway',
diff --git a/lib/server/service/business/project-version.js b/lib/server/service/business/project-version.js
index fd5e9b7cc..a993c3861 100644
--- a/lib/server/service/business/project-version.js
+++ b/lib/server/service/business/project-version.js
@@ -81,7 +81,7 @@ const create = async (data) => {
}
const update = async (id, data) => {
- return getRepository(ProjectVersion).update(id, data)
+ return getRepository(ProjectVersion).update({ id }, data)
}
const getList = async (projectId, fields = []) => {
diff --git a/lib/server/service/common/data-service.js b/lib/server/service/common/data-service.js
index b49687436..03e8d10ab 100644
--- a/lib/server/service/common/data-service.js
+++ b/lib/server/service/common/data-service.js
@@ -253,7 +253,7 @@ export function getDataService (name = 'default', customEntityMap) {
*/
async has (tableFileName, query = { deleteFlag: 0 }) {
const repository = getRepositoryByName(tableFileName)
- const count = await repository.count(transformQuery(query))
+ const count = await repository.count({ where: transformQuery(query) })
return count > 0
},
@@ -265,7 +265,7 @@ export function getDataService (name = 'default', customEntityMap) {
*/
count (tableFileName, query = { deleteFlag: 0 }) {
const repository = getRepositoryByName(tableFileName)
- return repository.count(transformQuery(query))
+ return repository.count({ where: transformQuery(query) })
},
/**
@@ -314,7 +314,7 @@ export function getDataService (name = 'default', customEntityMap) {
*/
findOne (tableFileName, query = { deleteFlag: 0 }) {
const repository = getRepositoryByName(tableFileName)
- return repository.findOne(transformQuery(query)) || {}
+ return repository.findOne({ where: transformQuery(query) }) || {}
},
/**
* 添加
@@ -341,7 +341,7 @@ export function getDataService (name = 'default', customEntityMap) {
* @returns 新增结果
*/
async bulkAdd (tableFileName, dataList) {
- dataList.forEach(item =>{
+ dataList.forEach(item => {
item.id = null
})
const repository = getRepositoryByName(tableFileName)
@@ -365,7 +365,7 @@ export function getDataService (name = 'default', customEntityMap) {
const repository = getRepositoryByName(tableFileName)
try {
const { id, ...rest } = data
- const { affected = 0 } = await repository.update(transformQuery(where), transformData(rest, repository.metadata.columns))
+ const { affected = 0 } = await repository.update({ where: transformQuery(where) }, transformData(rest, repository.metadata.columns))
return affected
} catch (error) {
throw new Error(error.sqlMessage || error)
@@ -392,7 +392,7 @@ export function getDataService (name = 'default', customEntityMap) {
}
}
try {
- const { affected = 0 } = await repository.update(data.id, transformData(editData, repository.metadata.columns))
+ const { affected = 0 } = await repository.update({ where: { id: data.id } }, transformData(editData, repository.metadata.columns))
return affected
} catch (error) {
throw new Error(error.sqlMessage || error)
@@ -425,7 +425,7 @@ export function getDataService (name = 'default', customEntityMap) {
try {
const transformList = transformData(editDataList, repository.metadata.columns)
- const updateResult = await Promise.all(transformList.map((data) => repository.update(data.id, data)))
+ const updateResult = await Promise.all(transformList.map((data) => repository.update({ where: { id: data.id } }, data)))
return updateResult.reduce((acc, { affected }) => {
acc += affected
return acc
@@ -453,7 +453,9 @@ export function getDataService (name = 'default', customEntityMap) {
throw new Error(global.i18n.t('删除 {{n}} 表数据的时候,需要使用 unique 或者 id 字段保证删除数据唯一', { n: tableFileName }))
}
try {
- const { affected = 0 } = await repository.delete(data)
+ // TypeORM 0.3+ 需要将条件放在 { where: ... } 中
+ const deleteCondition = typeof data === 'object' ? { where: data } : { where: { id: data } }
+ const { affected = 0 } = await repository.delete(deleteCondition)
return affected
} catch (error) {
throw new Error(error.sqlMessage || error)
@@ -470,7 +472,16 @@ export function getDataService (name = 'default', customEntityMap) {
if (isEmpty(datas)) throw new Error(global.i18n.t('删除 {{n}} 表数据的时候,不能传入空数据', { n: tableFileName }))
const repository = getRepositoryByName(tableFileName)
try {
- const { affected = 0 } = await repository.delete(datas)
+ // TypeORM 0.3+ 需要将条件放在 { where: ... } 中
+ // 如果 datas 是数组,需要判断是 ID 数组还是对象数组
+ const deleteCondition = Array.isArray(datas) && typeof datas[0] !== 'object'
+ ? { where: { id: In(datas) } }
+ : Array.isArray(datas) && typeof datas[0] === 'object'
+ ? { where: datas[0] } // 对象数组,取第一个对象作为条件(通常批量删除使用相同条件)
+ : typeof datas === 'object'
+ ? { where: datas }
+ : { where: { id: datas } }
+ const { affected = 0 } = await repository.delete(deleteCondition)
return affected
} catch (error) {
throw new Error(error.sqlMessage || error)
@@ -568,6 +579,7 @@ export const TABLE_FILE_NAME = {
FLOW_TASK: 'flow-task',
API: 'api',
API_CATEGORY: 'api-category',
+ API_DOMAINS: 'api-domains',
FILE: 'file',
FUNC_API: 'func-api',
QUERY_RECORD: 'query-record',
diff --git a/lib/server/utils/validate-url.js b/lib/server/utils/validate-url.js
new file mode 100644
index 000000000..9ba917b94
--- /dev/null
+++ b/lib/server/utils/validate-url.js
@@ -0,0 +1,124 @@
+const dns = require('dns').promises
+const ipaddr = require('ipaddr.js')
+
+function isPrivateIP (ip) {
+ if (ip === '127.0.0.1' && process.env.NODE_ENV === 'development') {
+ return false
+ }
+ let addr
+ try {
+ addr = ipaddr.parse(ip)
+ } catch (e) {
+ return true
+ }
+
+ if (addr.kind() === 'ipv4') {
+ const range = addr.range()
+ return (
+ range === 'private'
+ || range === 'loopback'
+ || range === 'linkLocal'
+ || range === 'broadcast'
+ || range === 'carrierGradeNat'
+ || range === 'unspecified'
+ )
+ } else if (addr.kind() === 'ipv6') {
+ return (
+ addr.isLoopback()
+ || addr.range() === 'uniqueLocal'
+ || addr.range() === 'linkLocal'
+ )
+ }
+ return false
+}
+
+function checkPort (port) {
+ port = parseInt(port)
+ // 禁止端口
+ const forbiddenPort = [21, 22, 23, 25, 69, 135, 137, 138, 139,
+ 161, 162, 389, 465, 514, 587, 636, 873, 1099, 2181, 2375,
+ 2376, 27017, 3306, 3389, 36000, 4848, 50070, 50075, 5432,
+ 56000, 5900, 5901, 6379, 7001, 7002, 9200, 9300, 10050, 10051,
+ 10250, 10255, 11211]
+ return forbiddenPort.includes(port)
+}
+
+// 获取当前环境的主域名
+function getMainDomain (origin) {
+ try {
+ const url = new URL(origin)
+ const hostnameParts = url?.hostname?.split('.')
+ const partsLength = hostnameParts?.length
+
+ // 如果域名部分少于2个,返回null
+ if (partsLength < 2) {
+ return null
+ }
+
+ // 提取主域名
+ const mainDomain = hostnameParts.slice(partsLength - 2).join('.')
+ return `.${mainDomain}`
+ } catch (error) {
+ console.error('Invalid URL:', error)
+ return null
+ }
+}
+
+export const validateUrl = async (userInputUrl, originalUrl, whiteList = []) => {
+ let parsedUrl = ''
+ try {
+ parsedUrl = new URL(userInputUrl)
+ } catch (err) {
+ return {
+ result: false,
+ message: 'url异常'
+ }
+ }
+
+ // 只允许 http 和 https 协议
+ if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
+ return {
+ result: false,
+ message: '只允许http跟https协议'
+ }
+ }
+
+ // 域名必须在白名单
+ const domain = getMainDomain(originalUrl)
+ if (!domain) {
+ return {
+ result: false,
+ message: '无法解析需要访问的域名'
+ }
+ }
+ const targetDomain = getMainDomain(userInputUrl)
+ if (!parsedUrl.hostname?.endsWith(domain) && !whiteList?.includes(targetDomain)) {
+ return {
+ result: false,
+ message: '只允许访问跟当前环境同域的域名'
+ }
+ }
+
+ const port = parsedUrl.port
+ if (port && checkPort(port)) {
+ return {
+ result: false,
+ message: '禁止使用此端口'
+ }
+ }
+ // 解析域名对应的 IP 地址,防止 DNS 解析绕过
+ const addresses = await dns.lookup(parsedUrl.hostname, { all: true })
+ for (const addr of addresses) {
+ if (isPrivateIP(addr.address)) {
+ return {
+ result: false,
+ message: '禁止访问私有ip'
+ }
+ }
+ }
+
+ return {
+ result: true,
+ message: ''
+ }
+}
diff --git a/lib/shared/constant.js b/lib/shared/constant.js
index 6fc7c7d9c..dc73a548e 100644
--- a/lib/shared/constant.js
+++ b/lib/shared/constant.js
@@ -9,7 +9,6 @@
* specific language governing permissions and limitations under the License.
*/
-export const FORBIDDEN_KEYS = ['__proto__', 'constructor', 'prototype']
/**
* 数据类型
*/
diff --git a/lib/shared/page-code/common/modelMethods.js b/lib/shared/page-code/common/modelMethods.js
index 80b378e61..86cc794e0 100644
--- a/lib/shared/page-code/common/modelMethods.js
+++ b/lib/shared/page-code/common/modelMethods.js
@@ -2,6 +2,7 @@ import { getValue, getMethodByCode, getValueType } from './utils'
import { uuid, sharedI18n, toPascal, capitalizeFirstChar } from '../../util'
import { EVENT_ACTION_TYPE } from '../../function/constant'
import { camelCase, camelCaseTransformMerge } from 'change-case'
+import { safeSetProperty } from '../../security/property-injection-guard'
/**
* @desc 追加样式,将页面每个元素的样式追加到css中
@@ -29,13 +30,10 @@ export function appendCss (code, cssStr) {
* @param { String } value 需要添加的变量value
*/
export function dataTemplate (code, key, value) {
- const FORBIDDEN_KEYS = ['__proto__', 'constructor', 'prototype']
- // 只允许字母数字和下划线,且不允许空字符串
- const SAFE_KEY_REGEX = /^[a-zA-Z0-9_-]+$/
- if (typeof key !== 'string' || FORBIDDEN_KEYS.includes(key) || !SAFE_KEY_REGEX?.test(key)) {
- throw new Error('Invalid or forbidden key')
+ // 使用统一的安全工具函数来设置属性,防止原型链污染
+ if (!safeSetProperty(code.dataObj, key, value)) {
+ throw new Error(`Invalid or forbidden key: ${key}`)
}
- Object.assign(code.dataObj, { [key]: value })
}
/**
diff --git a/lib/shared/page-code/common/utils.js b/lib/shared/page-code/common/utils.js
index 4ce2e07dd..95ba80bd9 100644
--- a/lib/shared/page-code/common/utils.js
+++ b/lib/shared/page-code/common/utils.js
@@ -3,6 +3,8 @@ import { unitFilter } from '../../util'
import safeStringify from '../../../client/src/common/json-safe-stringify'
+import { isSafePropertyKey } from '../../security/property-injection-guard'
+
/**
* @desc 判断输入值的类型
* @param { String } val 需要判读的值
@@ -60,8 +62,8 @@ export function getMethodByCode (methodCode, funcGroups = []) {
export function getValue (val) {
let value = val
const type = getValueType(val)
- const logVal = val?.toString()?.replace(/[\n\r]/g, '').slice(0, 50)
- console.log('typefromval:', type, logVal)
+ // const logVal = val?.toString()?.replace(/[\n\r]/g, '').slice(0, 50)
+ // console.log('typefromval:', type, logVal)
switch (type) {
case 'string':
case 'undefined':
@@ -101,14 +103,22 @@ export function transformToString (val) {
*/
export function handleRenderStyles (renderStyles = {}) {
const styles = {}
- if (renderStyles && typeof renderStyles === 'object' && Object.keys(renderStyles).length > 0) {
- if (renderStyles['customStyle']) {
- Object.assign(renderStyles, renderStyles['customStyle'])
- delete renderStyles['customStyle']
+ // 创建副本,避免原对象被污染
+ const tempRenderStyles = { ...renderStyles }
+ if (tempRenderStyles && typeof tempRenderStyles === 'object' && Object.keys(tempRenderStyles).length > 0) {
+ if (tempRenderStyles['customStyle']) {
+ // 只合并安全的 customStyle key
+ Object.keys(tempRenderStyles['customStyle']).forEach(key => {
+ if (isSafePropertyKey(key)) {
+ tempRenderStyles[key] = tempRenderStyles['customStyle'][key]
+ }
+ })
+ delete tempRenderStyles['customStyle']
}
- for (const key in renderStyles) {
- if (renderStyles[key]) {
- Object.assign(styles, { [paramCase(key)]: unitFilter(renderStyles[key]) })
+ for (const key in tempRenderStyles) {
+ if (!isSafePropertyKey(key)) continue
+ if (tempRenderStyles[key]) {
+ Object.assign(styles, { [paramCase(key)]: unitFilter(tempRenderStyles[key]) })
}
}
}
diff --git a/lib/shared/page-code/template/page/layout/index.js b/lib/shared/page-code/template/page/layout/index.js
index f12123414..a3c1bfeef 100644
--- a/lib/shared/page-code/template/page/layout/index.js
+++ b/lib/shared/page-code/template/page/layout/index.js
@@ -32,11 +32,11 @@ export default function generateLayout (code, items = [], inFreeLayout = false)
// 多列布局
if (item.type === 'render-grid') {
/* eslint-disable no-unused-vars, indent */
- getItemStyles(code, item.componentId, item.renderStyles, item.renderProps, { inFreeLayout })
+ getItemStyles(code, item.componentId, item.renderStyles, item.renderProps, { inFreeLayout, cssNamePrefix: `.bk-layout-row-${code.uniqueKey}` })
templateCode += `