From 5e11fa1cadb6ff99b3bbd067838816802b038bb7 Mon Sep 17 00:00:00 2001 From: terlinhe Date: Wed, 9 Jul 2025 22:51:56 +0800 Subject: [PATCH 01/12] =?UTF-8?q?fix:=20=E9=99=90=E5=88=B6server=E8=AF=B7?= =?UTF-8?q?=E6=B1=82url=20#=20Reviewed,=20transaction=20id:=2049711?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/server/controller/mock-data.js | 24 +++- .../lib/server/controller/mock-data.js | 24 +++- .../lib/server/util/validate-url.js | 117 ++++++++++++++++++ .../lib/server/controller/mock-data.js | 24 +++- .../lib/server/util/validate-url.js | 117 ++++++++++++++++++ lib/server/utils/validate-url.js | 117 ++++++++++++++++++ 6 files changed, 414 insertions(+), 9 deletions(-) create mode 100644 lib/server/project-template/vue2/project-init-code/lib/server/util/validate-url.js create mode 100644 lib/server/project-template/vue3/project-init-code/lib/server/util/validate-url.js create mode 100644 lib/server/utils/validate-url.js diff --git a/lib/server/controller/mock-data.js b/lib/server/controller/mock-data.js index 47cf6ccd0..f8e2dacb6 100644 --- a/lib/server/controller/mock-data.js +++ b/lib/server/controller/mock-data.js @@ -15,6 +15,7 @@ import { LCDataService, TABLE_FILE_NAME } from '../service/common/data-service' +import { validateUrl } from '../utils/validate-url' const METHODS_WITH_DATA = ['post', 'put', 'patch'] @@ -22,7 +23,6 @@ const Data = { async getApiData (ctx) { try { const { - url, projectId, type = 'get', withToken = 0, @@ -31,15 +31,33 @@ const Data = { axiosConfig = {} } = ctx.request.body || {} + let url = ctx.request.body?.url + url = url?.replaceAll('@', '') + // /开头的是相对路径 + if (url.startsWith('/')) { + url = ctx.origin + url + } + + const checkUrl = await validateUrl(url, ctx.origin) + + if (checkUrl?.result !== true) { + ctx.throwError({ + message: checkUrl?.message || '请求异常' + }) + } + // 发送请求的参数 const httpConf = { ...axiosConfig, - url: /^http(s)?\:\/\//.test(url) ? url : ctx.origin + url, + url, method: type, headers: { ...axiosConfig?.headers, 'X-PROJECT-ID': projectId - } + }, + // 不允许重定向 + maxRedirects: 0, + timeout: 5000 } // 请求携带的数据 diff --git a/lib/server/project-template/vue2/project-init-code/lib/server/controller/mock-data.js b/lib/server/project-template/vue2/project-init-code/lib/server/controller/mock-data.js index be682c291..28852df8a 100644 --- a/lib/server/project-template/vue2/project-init-code/lib/server/controller/mock-data.js +++ b/lib/server/project-template/vue2/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 + } + + const checkUrl = await validateUrl(url, ctx.origin) + + 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/vue2/project-init-code/lib/server/util/validate-url.js b/lib/server/project-template/vue2/project-init-code/lib/server/util/validate-url.js new file mode 100644 index 000000000..79f31bd11 --- /dev/null +++ b/lib/server/project-template/vue2/project-init-code/lib/server/util/validate-url.js @@ -0,0 +1,117 @@ +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) => { + 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 || !parsedUrl.hostname?.endsWith(domain)) { + 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/lib/server/controller/mock-data.js b/lib/server/project-template/vue3/project-init-code/lib/server/controller/mock-data.js index be682c291..28852df8a 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 + } + + const checkUrl = await validateUrl(url, ctx.origin) + + 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/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..79f31bd11 --- /dev/null +++ b/lib/server/project-template/vue3/project-init-code/lib/server/util/validate-url.js @@ -0,0 +1,117 @@ +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) => { + 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 || !parsedUrl.hostname?.endsWith(domain)) { + 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/utils/validate-url.js b/lib/server/utils/validate-url.js new file mode 100644 index 000000000..79f31bd11 --- /dev/null +++ b/lib/server/utils/validate-url.js @@ -0,0 +1,117 @@ +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) => { + 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 || !parsedUrl.hostname?.endsWith(domain)) { + 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: '' + } +} From b7380b9898c1a0e07896320ab612ddf0a08d1da6 Mon Sep 17 00:00:00 2001 From: terlinhe Date: Mon, 14 Jul 2025 21:06:36 +0800 Subject: [PATCH 02/12] =?UTF-8?q?fix:=20unzip=E8=A7=A3=E5=8E=8B=E5=A4=84?= =?UTF-8?q?=E7=90=86=20#=20Reviewed,=20transaction=20id:=2050255?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/server/controller/component.js | 44 ++++++++++++++++--- .../lib/server/controller/mock-data.js | 2 +- .../lib/server/controller/mock-data.js | 2 +- lib/shared/util.js | 32 ++++++++++++++ 4 files changed, 73 insertions(+), 7 deletions(-) diff --git a/lib/server/controller/component.js b/lib/server/controller/component.js index 94de146a9..a95c5e55b 100644 --- a/lib/server/controller/component.js +++ b/lib/server/controller/component.js @@ -14,7 +14,7 @@ import { RequestContext } from '../middleware/request-context' import OperationLogger from '../service/common/operation-logger' import { POST_COMPONENT_CREATE, POST_COMPONENT_UPDATE } from '../system-conf/operate-log' import { whereVersionLiteral } from '../model/common' -import { isMatchFramework } from '../../shared/util' +import { isMatchFramework, filterFilePath } from '../../shared/util' import { checkInServer } from './iam' // 所有组件 @@ -663,9 +663,45 @@ export const compDelete = async (ctx) => { } } +// 解压校验处理 +async function safeUnzip (zipFilePath, destDir) { + const unzipper = require('unzipper') + const path = require('path') + const fs = require('fs') + const fse = require('fs-extra') + + await fse.ensureDir(destDir) + + const directory = await unzipper.Open.file(zipFilePath) + for (const file of directory.files) { + const filePath = filterFilePath(file.path) + if (!filePath) { + // 文件名清理后为空,跳过或抛错 + throw new Error('文件名非法,无法解压') + } + + const absolutePath = path.resolve(destDir, filePath) + if (!absolutePath.startsWith(destDir)) { + throw new Error('Zip 包包含非法路径') + } + + if (file.type === 'Directory') { + await fse.ensureDir(absolutePath) + } else { + await fse.ensureDir(path.dirname(absolutePath)) + const readStream = file.stream() + const writeStream = fs.createWriteStream(absolutePath) + await new Promise((resolve, reject) => { + readStream.pipe(writeStream) + .on('finish', resolve) + .on('error', reject) + }) + } + } +} + // 上传组件 export const upload = async (ctx) => { - const unzipper = require('unzipper') const path = require('path') const fs = require('fs') @@ -697,9 +733,7 @@ export const upload = async (ctx) => { const tempDir = path.resolve(__dirname, '../temp/') const componentDestDir = path.resolve(tempDir, `${componentPath}`) - await fs.createReadStream(uploadComponent.path) - .pipe(unzipper.Extract({ path: componentDestDir })) - .promise() + await safeUnzip(uploadComponent.path, componentDestDir) // 删除mac压缩 隐藏文件 const fse = require('fs-extra') diff --git a/lib/server/project-template/vue2/project-init-code/lib/server/controller/mock-data.js b/lib/server/project-template/vue2/project-init-code/lib/server/controller/mock-data.js index 28852df8a..7cdec1e7e 100644 --- a/lib/server/project-template/vue2/project-init-code/lib/server/controller/mock-data.js +++ b/lib/server/project-template/vue2/project-init-code/lib/server/controller/mock-data.js @@ -33,7 +33,7 @@ export default class DataController { url = url?.replaceAll('@', '') // /开头的是相对路径 if (url.startsWith('/')) { - url = hostUrl + url = hostUrl + url } const checkUrl = await validateUrl(url, ctx.origin) 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 28852df8a..7cdec1e7e 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 @@ -33,7 +33,7 @@ export default class DataController { url = url?.replaceAll('@', '') // /开头的是相对路径 if (url.startsWith('/')) { - url = hostUrl + url = hostUrl + url } const checkUrl = await validateUrl(url, ctx.origin) diff --git a/lib/shared/util.js b/lib/shared/util.js index c446691d7..4a646c97d 100644 --- a/lib/shared/util.js +++ b/lib/shared/util.js @@ -434,3 +434,35 @@ export const getLesscodeVarCode = (value) => { } return '' } + +// 过滤文件路径中危险字符 +export function filterFilePath (inputPath, options = {}) { + // 默认把危险字符替换成_ + const { replacement = '_' } = options + + if (typeof inputPath !== 'string') { + throw new TypeError('inputPath 必须是字符串') + } + + // 拆分路径,过滤空和 '.',禁止 '..' + const parts = inputPath.split('/').filter(part => part && part !== '.') + + if (parts.includes('..')) { + throw new Error('路径中不允许包含父目录引用 ".."') + } + + // 定义危险字符正则(控制字符 + 常见危险符号) + // 这里排除了 '/' 因为已经拆分了 + const dangerousCharsPattern = '[\\u0000-\\u001F\\\\*?"\'<>|&;$(){}\\[\\]~!#%+=^:, ]' + const dangerousCharsRegex = new RegExp(dangerousCharsPattern, 'g') + + const safeParts = parts.map(part => { + const replaced = part.replace(dangerousCharsRegex, replacement) + if (!replaced) { + throw new Error(`路径段 "${part}" 过滤后为空,可能全部是非法字符`) + } + return replaced + }) + + return safeParts.join('/') +} From 7b0cfde9f462c2c03897ffcbd2a6d508818328d4 Mon Sep 17 00:00:00 2001 From: terlinhe Date: Tue, 16 Sep 2025 16:45:53 +0800 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20--story=3D127136780=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=9C=A8=E6=95=B0=E6=8D=AE=E5=BA=93=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=85=81=E8=AE=B8=E8=BF=9C=E7=A8=8B=E5=87=BD=E6=95=B0=E8=AE=BF?= =?UTF-8?q?=E9=97=AE=E7=9A=84=E4=B8=BB=E5=9F=9F=E5=90=8D=20#=20Reviewed,?= =?UTF-8?q?=20transaction=20id:=2058097?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../views/system/components/project-form.vue | 2 +- lib/server/controller/mock-data.js | 14 ++++- lib/server/model/entities/api-domains.js | 20 +++++++ .../migrations/20250905024602-update-sql.js | 53 +++++++++++++++++++ .../sqls/20250905024602-update-sql-down.sql | 1 + .../sqls/20250905024602-update-sql-up.sql | 11 ++++ lib/server/model/project-code.js | 13 +++-- .../lib/server/controller/mock-data.js | 2 +- .../lib/server/util/validate-url.js | 11 +++- .../lib/server/controller/mock-data.js | 2 +- .../lib/server/util/validate-url.js | 11 +++- lib/server/service/common/data-service.js | 3 +- lib/server/utils/validate-url.js | 11 +++- lib/shared/page-code/common/utils.js | 4 +- lib/shared/util.js | 18 ++++--- package.json | 4 +- 16 files changed, 155 insertions(+), 25 deletions(-) create mode 100644 lib/server/model/entities/api-domains.js create mode 100644 lib/server/model/migrations/20250905024602-update-sql.js create mode 100644 lib/server/model/migrations/sqls/20250905024602-update-sql-down.sql create mode 100644 lib/server/model/migrations/sqls/20250905024602-update-sql-up.sql diff --git a/lib/client/src/views/system/components/project-form.vue b/lib/client/src/views/system/components/project-form.vue index 67e74ba0d..87dcb0e77 100644 --- a/lib/client/src/views/system/components/project-form.vue +++ b/lib/client/src/views/system/components/project-form.vue @@ -5,7 +5,7 @@ - diff --git a/lib/server/controller/mock-data.js b/lib/server/controller/mock-data.js index f8e2dacb6..889fab7fb 100644 --- a/lib/server/controller/mock-data.js +++ b/lib/server/controller/mock-data.js @@ -34,11 +34,21 @@ const Data = { let url = ctx.request.body?.url url = url?.replaceAll('@', '') // /开头的是相对路径 - if (url.startsWith('/')) { + if (url?.startsWith('/')) { url = ctx.origin + url } - const checkUrl = await validateUrl(url, ctx.origin) + if (!url) { + ctx.throwError({ + message: 'url异常' + }) + } + + const { list } = await LCDataService.get({ + tableFileName: TABLE_FILE_NAME.API_DOMAINS + }) + const whiteList = list.map(item => item.domain) + const checkUrl = await validateUrl(url, ctx.origin, whiteList) if (checkUrl?.result !== true) { ctx.throwError({ diff --git a/lib/server/model/entities/api-domains.js b/lib/server/model/entities/api-domains.js new file mode 100644 index 000000000..efd624a36 --- /dev/null +++ b/lib/server/model/entities/api-domains.js @@ -0,0 +1,20 @@ +/** + * 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 { Entity, Column } from 'typeorm' +import Base from './base' + +@Entity({ name: 'api_domains', comment: 'api域名白名单' }) +export default class extends Base { + // 域名 + @Column({ type: 'varchar', length: 50 }) + domain +} diff --git a/lib/server/model/migrations/20250905024602-update-sql.js b/lib/server/model/migrations/20250905024602-update-sql.js new file mode 100644 index 000000000..a06dd5209 --- /dev/null +++ b/lib/server/model/migrations/20250905024602-update-sql.js @@ -0,0 +1,53 @@ +'use strict'; + +var dbm; +var type; +var seed; +var fs = require('fs'); +var path = require('path'); +var Promise; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function(options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; + Promise = options.Promise; +}; + +exports.up = function(db) { + var filePath = path.join(__dirname, 'sqls', '20250905024602-update-sql-up.sql'); + return new Promise( function( resolve, reject ) { + fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){ + if (err) return reject(err); + console.log('received data: ' + data); + + resolve(data); + }); + }) + .then(function(data) { + return db.runSql(data); + }); +}; + +exports.down = function(db) { + var filePath = path.join(__dirname, 'sqls', '20250905024602-update-sql-down.sql'); + return new Promise( function( resolve, reject ) { + fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){ + if (err) return reject(err); + console.log('received data: ' + data); + + resolve(data); + }); + }) + .then(function(data) { + return db.runSql(data); + }); +}; + +exports._meta = { + "version": 1 +}; diff --git a/lib/server/model/migrations/sqls/20250905024602-update-sql-down.sql b/lib/server/model/migrations/sqls/20250905024602-update-sql-down.sql new file mode 100644 index 000000000..44f074ea8 --- /dev/null +++ b/lib/server/model/migrations/sqls/20250905024602-update-sql-down.sql @@ -0,0 +1 @@ +/* Replace with your SQL commands */ \ No newline at end of file diff --git a/lib/server/model/migrations/sqls/20250905024602-update-sql-up.sql b/lib/server/model/migrations/sqls/20250905024602-update-sql-up.sql new file mode 100644 index 000000000..0aa00b2ee --- /dev/null +++ b/lib/server/model/migrations/sqls/20250905024602-update-sql-up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `api_domains` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增唯一主键。系统保留字段,不可修改', + `createTime` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '系统会默认写入数据创建时间。系统保留字段,不可修改', + `createUser` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '系统会默认写入数据创建人。系统保留字段,不可修改', + `updateTime` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '系统会默认写入数据更新时间。系统保留字段,不可修改', + `updateUser` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '系统会默认写入数据更新人。系统保留字段,不可修改', + `domain` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '本条需求的唯一标识', + `deleteFlag` int(11) DEFAULT 0 COMMENT '是否删除,1代表已删除', + PRIMARY KEY (`id`), + INDEX `id`(`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; \ No newline at end of file diff --git a/lib/server/model/project-code.js b/lib/server/model/project-code.js index 31a305fb5..5555d7a61 100644 --- a/lib/server/model/project-code.js +++ b/lib/server/model/project-code.js @@ -648,7 +648,8 @@ const projectCode = { projectId ) - const isGenerateApigw = flowList.length > 0 || flowTplList.length > 0 || hasBkvision + // 有数据表的也要生成网关,因为有可能需要直接被bkvision作为数据源引用 + const isGenerateApigw = flowList.length > 0 || flowTplList.length > 0 || hasBkvision || dataTables.length await this.generateDataSource(dataTables, dataTableModifyRecords, targetPath, isGenerateApigw, STATIC_URL, thirdPartDBs) await this.generateFileByReplace( path.join(targetPath, 'lib/server/service/form.js'), @@ -906,6 +907,10 @@ const projectCode = { writePackageJSON (packageJsonFilePath, deps, projectId = 0) { return new Promise(async (resolve, reject) => { + const { list } = await LCDataService.get({ + tableFileName: TABLE_FILE_NAME.API_DOMAINS + }) + const whiteList = list.map(item => item.domain) fse.ensureFile(packageJsonFilePath).then(async () => { const str = await fs.readFileSync(packageJsonFilePath, 'utf8') const ret = JSON.parse(str) @@ -925,7 +930,8 @@ const projectCode = { BKPAAS_ENGINE_REGION: global.AUTH_NAME === 'bk_ticket' ? 'ieod' : 'default', BK_API_URL_TMPL: httpConf.apiGateWayUrlTmpl || '', BK_PROJECT_ID: projectId, // 应用在lesscode的id - BK_PROJECT_CODE: project?.projectCode + BK_PROJECT_CODE: project?.projectCode, + BK_API_DOMAIN_WHITE_LIST: whiteList || [] }) Object.assign(ret.betterScripts.build.env, { BK_LOGIN_URL: httpConf.loginUrl, @@ -935,7 +941,8 @@ const projectCode = { BKPAAS_ENGINE_REGION: global.AUTH_NAME === 'bk_ticket' ? 'ieod' : 'default', BK_API_URL_TMPL: httpConf.apiGateWayUrlTmpl || '', BK_PROJECT_ID: projectId, // 应用在lesscode的id - BK_PROJECT_CODE: project?.projectCode + BK_PROJECT_CODE: project?.projectCode, + BK_API_DOMAIN_WHITE_LIST: whiteList || [] }) } diff --git a/lib/server/project-template/vue2/project-init-code/lib/server/controller/mock-data.js b/lib/server/project-template/vue2/project-init-code/lib/server/controller/mock-data.js index 7cdec1e7e..c60c5bfb9 100644 --- a/lib/server/project-template/vue2/project-init-code/lib/server/controller/mock-data.js +++ b/lib/server/project-template/vue2/project-init-code/lib/server/controller/mock-data.js @@ -36,7 +36,7 @@ export default class DataController { url = hostUrl + url } - const checkUrl = await validateUrl(url, ctx.origin) + const checkUrl = await validateUrl(url, ctx.origin, process.env.BK_API_DOMAIN_WHITE_LIST) if (checkUrl?.result !== true) { ctx.throwError({ diff --git a/lib/server/project-template/vue2/project-init-code/lib/server/util/validate-url.js b/lib/server/project-template/vue2/project-init-code/lib/server/util/validate-url.js index 79f31bd11..cfd9a26ee 100644 --- a/lib/server/project-template/vue2/project-init-code/lib/server/util/validate-url.js +++ b/lib/server/project-template/vue2/project-init-code/lib/server/util/validate-url.js @@ -64,7 +64,7 @@ function getMainDomain (origin) { } } -export const validateUrl = async (userInputUrl, originalUrl) => { +export const validateUrl = async (userInputUrl, originalUrl, whiteList) => { let parsedUrl = '' try { parsedUrl = new URL(userInputUrl) @@ -85,7 +85,14 @@ export const validateUrl = async (userInputUrl, originalUrl) => { // 域名必须在白名单 const domain = getMainDomain(originalUrl) - if (!domain || !parsedUrl.hostname?.endsWith(domain)) { + if (!domain) { + return { + result: false, + message: '无法解析需要访问的域名' + } + } + const targetDomain = getMainDomain(userInputUrl) + if (!parsedUrl.hostname?.endsWith(domain) && !whiteList?.includes(targetDomain)) { return { result: false, message: '只允许访问跟当前环境同域的域名' 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 7cdec1e7e..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 @@ -36,7 +36,7 @@ export default class DataController { url = hostUrl + url } - const checkUrl = await validateUrl(url, ctx.origin) + const checkUrl = await validateUrl(url, ctx.origin, process.env.BK_API_DOMAIN_WHITE_LIST) if (checkUrl?.result !== true) { ctx.throwError({ 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 index 79f31bd11..9ba917b94 100644 --- 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 @@ -64,7 +64,7 @@ function getMainDomain (origin) { } } -export const validateUrl = async (userInputUrl, originalUrl) => { +export const validateUrl = async (userInputUrl, originalUrl, whiteList = []) => { let parsedUrl = '' try { parsedUrl = new URL(userInputUrl) @@ -85,7 +85,14 @@ export const validateUrl = async (userInputUrl, originalUrl) => { // 域名必须在白名单 const domain = getMainDomain(originalUrl) - if (!domain || !parsedUrl.hostname?.endsWith(domain)) { + if (!domain) { + return { + result: false, + message: '无法解析需要访问的域名' + } + } + const targetDomain = getMainDomain(userInputUrl) + if (!parsedUrl.hostname?.endsWith(domain) && !whiteList?.includes(targetDomain)) { return { result: false, message: '只允许访问跟当前环境同域的域名' diff --git a/lib/server/service/common/data-service.js b/lib/server/service/common/data-service.js index b49687436..9f5e19773 100644 --- a/lib/server/service/common/data-service.js +++ b/lib/server/service/common/data-service.js @@ -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) @@ -568,6 +568,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 index 79f31bd11..9ba917b94 100644 --- a/lib/server/utils/validate-url.js +++ b/lib/server/utils/validate-url.js @@ -64,7 +64,7 @@ function getMainDomain (origin) { } } -export const validateUrl = async (userInputUrl, originalUrl) => { +export const validateUrl = async (userInputUrl, originalUrl, whiteList = []) => { let parsedUrl = '' try { parsedUrl = new URL(userInputUrl) @@ -85,7 +85,14 @@ export const validateUrl = async (userInputUrl, originalUrl) => { // 域名必须在白名单 const domain = getMainDomain(originalUrl) - if (!domain || !parsedUrl.hostname?.endsWith(domain)) { + if (!domain) { + return { + result: false, + message: '无法解析需要访问的域名' + } + } + const targetDomain = getMainDomain(userInputUrl) + if (!parsedUrl.hostname?.endsWith(domain) && !whiteList?.includes(targetDomain)) { return { result: false, message: '只允许访问跟当前环境同域的域名' diff --git a/lib/shared/page-code/common/utils.js b/lib/shared/page-code/common/utils.js index 4ce2e07dd..fded43a40 100644 --- a/lib/shared/page-code/common/utils.js +++ b/lib/shared/page-code/common/utils.js @@ -60,8 +60,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': diff --git a/lib/shared/util.js b/lib/shared/util.js index 4a646c97d..434e0763a 100644 --- a/lib/shared/util.js +++ b/lib/shared/util.js @@ -168,13 +168,19 @@ export function throttle (fn, delay = 200) { * @returns */ export function unitFilter (value) { - if (value?.endsWith('rpx') && value.length < 50) { - const match = /(\d+)rpx$/.exec(value) - if (match) { - const sizeNumber = (parseInt(match[1], 10) / 750 * 20).toFixed(2) - const result = sizeNumber + 'rem' - return result + if (!value) return '' + try { + value = value?.toString() + if (value?.endsWith('rpx') && value.length < 50) { + const match = /(\d+)rpx$/.exec(value) + if (match) { + const sizeNumber = (parseInt(match[1], 10) / 750 * 20).toFixed(2) + const result = sizeNumber + 'rem' + return result + } } + } catch (error) { + console.log(error, 'unitfilter err') } return value } diff --git a/package.json b/package.json index 60a84d37f..3ce0ab4ec 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "@blueking/cli-service-webpack": "0.0.0-beta.81", "@blueking/crypto-js-sdk": "^0.0.5", "@blueking/login-modal": "^1.0.1", - "@blueking/notice-component-vue2": "^2.0.5", + "@blueking/notice-component-vue2": "^2.0.6", "@blueking/user-selector": "^1.0.7", "@blueking/platform-config": "1.0.5", "@blueking/chrome-tips": "0.0.3-beta.3", @@ -125,7 +125,7 @@ "db-migrate": "^0.11.12", "db-migrate-mysql": "^2.1.2", "dexie": "^3.2.3", - "dompurify": "^3.0.6", + "dompurify": "^3.2.6", "echarts": "~5.4.0", "element-ui": "~2.15.1", "enhanced-resolve": "^5.10.0", From 6d928b3751f2ac93193a3ed1f8a0979bd523ee31 Mon Sep 17 00:00:00 2001 From: terlinhe Date: Mon, 29 Sep 2025 20:46:41 +0800 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20--story=3D127795383=20vue3?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E9=A2=84=E8=A7=88=E6=97=B6=EF=BC=8C=20?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=E6=A0=B7=E5=BC=8F=E9=9A=94=E7=A6=BB=E4=B8=8D?= =?UTF-8?q?=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/shared/page-code/template/page/layout/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 += ` ${item.renderSlots && item.renderSlots.default && item.renderSlots.default.map(col => { - getItemStyles(code, col.componentId, col.renderStyles, col.renderProps, {}) + getItemStyles(code, col.componentId, col.renderStyles, col.renderProps, { cssNamePrefix: `.bk-layout-col-${code.uniqueKey}` }) const colAlignStr = getAlignStr(col.renderAlign || {}, inFreeLayout) getInsertCommonStyle(code, colAlignStr) const { @@ -56,7 +56,7 @@ export default function generateLayout (code, items = [], inFreeLayout = false) ` // 自由布局 } else if (['free-layout', 'h5-page'].includes(item.type)) { - getItemStyles(code, item.componentId, item.renderStyles, item.renderProps, { componentType: item.type }) + getItemStyles(code, item.componentId, item.renderStyles, item.renderProps, { componentType: item.type, cssNamePrefix: `.bk-free-layout-${code.uniqueKey}` }) templateCode += `
From e28ce2c3cc071f0f3ccb2962a0c789d9bbbf7649 Mon Sep 17 00:00:00 2001 From: terlinhe Date: Mon, 29 Sep 2025 20:49:11 +0800 Subject: [PATCH 05/12] =?UTF-8?q?fix:=20=E9=97=AE=E9=A2=98=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20#=20Reviewed,=20transaction=20id:=2059650?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/server/controller/component.js | 3 +-- lib/server/controller/project.js | 1 + lib/server/router/component.js | 4 ++-- lib/server/router/page.js | 2 +- lib/server/service/business/open-api.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/server/controller/component.js b/lib/server/controller/component.js index a95c5e55b..318147188 100644 --- a/lib/server/controller/component.js +++ b/lib/server/controller/component.js @@ -110,8 +110,7 @@ export const list = async (ctx) => { // 使用中的组件 export const useing = async (ctx) => { try { - const belongProjectId = ctx.request.headers['x-project-id'] - const { projectVersionId } = ctx.query + const { projectVersionId, belongProjectId } = ctx.query if (!belongProjectId) { throw new Error(global.i18n.t('应用id不能为空')) } diff --git a/lib/server/controller/project.js b/lib/server/controller/project.js index 3cbfbd970..2ddb26ab7 100644 --- a/lib/server/controller/project.js +++ b/lib/server/controller/project.js @@ -85,6 +85,7 @@ module.exports = { const userInfo = ctx.session.userInfo projectData.createUser = userInfo.username projectData.id = null + projectData.isOffcial = 0 const userProjectRoleData = { userId: userInfo.id, roleId: 1 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/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', From c294d1ffb58483127221ec5bd99ceeaedff2ad99 Mon Sep 17 00:00:00 2001 From: terlinhe Date: Tue, 30 Sep 2025 10:27:15 +0800 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20--story=3D118295653=20=E5=AF=B9?= =?UTF-8?q?=E6=8E=A5=E6=96=B0=E7=89=88bkvision=20#=20Reviewed,=20transacti?= =?UTF-8?q?on=20id:=2059666?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/client/src/api/index.js | 2 + lib/client/src/api/pureAxios.js | 2 +- .../patch/widget-bk-vision/index.vue | 53 ++----- .../components/render/pc/render-component.js | 2 + .../render/pc/widget/bk-vision/bk-vision.js | 138 ++++++++++++++++++ .../materials/vue2/bk/bk-vision/index.js | 2 +- .../materials/vue3/bk/bk-vision/index.js | 47 ++++++ .../materials/vue3/bk/index.js | 2 + lib/client/src/locales/lang/en.json | 2 +- lib/client/src/locales/lang/zh-cn.json | 2 +- lib/client/src/preview/component.js | 2 + lib/server/controller/bkvision.js | 2 +- .../src/components/patch/widget-bk-vision.vue | 47 ++---- .../lib/server/controller/bkvision.js | 2 +- .../src/components/patch/widget-bk-vision.vue | 51 ++----- .../lib/server/controller/bkvision.js | 2 +- 16 files changed, 246 insertions(+), 112 deletions(-) create mode 100644 lib/client/src/components/render/pc/widget/bk-vision/bk-vision.js create mode 100644 lib/client/src/element-materials/materials/vue3/bk/bk-vision/index.js 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/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/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/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/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/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/server/controller/bkvision.js b/lib/server/controller/bkvision.js index cd2bdb5ce..ebce118df 100644 --- a/lib/server/controller/bkvision.js +++ b/lib/server/controller/bkvision.js @@ -50,7 +50,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/vue2/project-init-code/lib/client/src/components/patch/widget-bk-vision.vue b/lib/server/project-template/vue2/project-init-code/lib/client/src/components/patch/widget-bk-vision.vue index aef64c76c..53d1474f0 100644 --- a/lib/server/project-template/vue2/project-init-code/lib/client/src/components/patch/widget-bk-vision.vue +++ b/lib/server/project-template/vue2/project-init-code/lib/client/src/components/patch/widget-bk-vision.vue @@ -19,29 +19,13 @@ }, 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 { - // app: {}, + visionApp: {}, apiPrefix: `${process.env.BK_AJAX_URL_PREFIX}/bkvision/`, - cdnPrefix: 'https://staticfile.qq.com/bkvision/p8e3a7f52d95c45d795cb6f90955f2800/latest/' + cdnPrefix: 'https://staticfile.qq.com/bkvision/pbb9b207ba200407982a9bd3d3f2895d4/latest/' } }, created () { @@ -58,11 +42,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() }, @@ -73,25 +55,26 @@ script.src = url document.body.append(script) script.onload = () => { - console.log('sdk load', file) resolve() } }) }, - initPanel () { + async initPanel () { if (window.BkVisionSDK) { - console.log('init bk-vision') - this.app = this.uid && window.BkVisionSDK.init(`#dashboard-${this.renderId}`, this.uid, { + 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, { apiPrefix: this.apiPrefix - // waterMark: { content: this.watchWark }, - // isFullScroll: this.isFullScroll, - // isShowTools: this.isShowTools, - // isShowRefresh: this.isShowRefresh, - // isShowTimeRange: this.isShowTimeRange }) - console.log(this.app, 'app inst') + console.log(this.visionApp, 'after init', this.uid) } else { - console.error(this.$t('sdk 加载异常')) + console.error('sdk 加载异常') } } } diff --git a/lib/server/project-template/vue2/project-init-code/lib/server/controller/bkvision.js b/lib/server/project-template/vue2/project-init-code/lib/server/controller/bkvision.js index 53a420f9d..a67aeefbb 100644 --- a/lib/server/project-template/vue2/project-init-code/lib/server/controller/bkvision.js +++ b/lib/server/project-template/vue2/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/client/src/components/patch/widget-bk-vision.vue b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/patch/widget-bk-vision.vue index aef64c76c..6ca6ed4fd 100644 --- a/lib/server/project-template/vue3/project-init-code/lib/client/src/components/patch/widget-bk-vision.vue +++ b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/patch/widget-bk-vision.vue @@ -16,32 +16,13 @@ uid: { type: String, default: '' - }, - 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 { - // app: {}, + visionApp: {}, apiPrefix: `${process.env.BK_AJAX_URL_PREFIX}/bkvision/`, - cdnPrefix: 'https://staticfile.qq.com/bkvision/p8e3a7f52d95c45d795cb6f90955f2800/latest/' + cdnPrefix: 'https://staticfile.qq.com/bkvision/pbb9b207ba200407982a9bd3d3f2895d4/latest/' } }, created () { @@ -58,11 +39,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() }, @@ -73,28 +52,28 @@ script.src = url document.body.append(script) script.onload = () => { - console.log('sdk load', file) resolve() } }) }, - initPanel () { + async initPanel () { if (window.BkVisionSDK) { - console.log('init bk-vision') - this.app = this.uid && window.BkVisionSDK.init(`#dashboard-${this.renderId}`, this.uid, { + 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, { apiPrefix: this.apiPrefix - // waterMark: { content: this.watchWark }, - // isFullScroll: this.isFullScroll, - // isShowTools: this.isShowTools, - // isShowRefresh: this.isShowRefresh, - // isShowTimeRange: this.isShowTimeRange }) - console.log(this.app, 'app inst') + console.log(this.visionApp, 'after init', this.uid) } else { - console.error(this.$t('sdk 加载异常')) + console.error('sdk 加载异常') } } } } - 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, From 145794b415d4b99e0246714249c7e8c0be63e8ce Mon Sep 17 00:00:00 2001 From: terlinhe Date: Sat, 11 Oct 2025 16:47:30 +0800 Subject: [PATCH 07/12] =?UTF-8?q?feat:=20=E5=8D=87=E7=BA=A7axios=E7=89=88?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/server/project-template/vue2/project-init-code/package.json | 2 +- lib/server/project-template/vue3/project-init-code/package.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/server/project-template/vue2/project-init-code/package.json b/lib/server/project-template/vue2/project-init-code/package.json index c042bd08c..09b5c143f 100644 --- a/lib/server/project-template/vue2/project-init-code/package.json +++ b/lib/server/project-template/vue2/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", 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..5b2bef8f2 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", diff --git a/package.json b/package.json index 3ce0ab4ec..c7b74185f 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "async-validator": "~1.8.1", "aws-sdk": "^2.736.0", "bk-lesscode-render": "1.0.0-beta.4", - "axios": "~1.6.0", + "axios": "~1.12.0", "babel-plugin-lodash": "^3.3.4", "babel-plugin-parameter-decorator": "^1.0.16", "better-npm-run": "~0.1.1", From e1c0ec7e1bbc10111507a517102ad6ead8771eb1 Mon Sep 17 00:00:00 2001 From: terlinhe Date: Sat, 11 Oct 2025 16:47:56 +0800 Subject: [PATCH 08/12] =?UTF-8?q?fix:=20=E6=96=87=E4=BB=B6=E5=86=99?= =?UTF-8?q?=E5=85=A5=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/server/model/vue-code.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/server/model/vue-code.js b/lib/server/model/vue-code.js index 4148a3856..79d7bdc2c 100644 --- a/lib/server/model/vue-code.js +++ b/lib/server/model/vue-code.js @@ -58,15 +58,13 @@ const deleteFile = filePath => { } async function createFile (filePath) { - const fs = require('fs').promises try { - await fs.writeFile(filePath, '', { encoding: 'utf8', flag: 'wx' }) + await fs.promises.writeFile(filePath, '', { flag: 'wx', encoding: 'utf8' }) } catch (err) { - if (err.code === 'EEXIST') { - console.log('文件已存在,跳过创建') - } else { - console.log('文件创建失败') + if (err.code !== 'EEXIST') { + throw err } + // If file exists, ignore } } From dea5c22b25a82ff645fe240343a042d35e4c9223 Mon Sep 17 00:00:00 2001 From: terlinhe Date: Mon, 13 Oct 2025 10:14:38 +0800 Subject: [PATCH 09/12] =?UTF-8?q?feat:=20=E5=87=BD=E6=95=B0=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E8=BF=87=E6=BB=A4=E6=9D=A1=E4=BB=B6=E6=94=AF=E6=8C=81?= =?UTF-8?q?funccode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/client/src/views/project/function-manage/children/list.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { From 3a9e655716702aef11805ed0b2789397fbcc351e Mon Sep 17 00:00:00 2001 From: terlinhe Date: Tue, 14 Oct 2025 16:39:23 +0800 Subject: [PATCH 10/12] =?UTF-8?q?feat:=20=E9=98=B2=E8=8C=83=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E8=BE=93=E5=85=A5=E5=85=AC=E5=85=B1=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E6=8A=BD=E5=8F=96=20#=20Reviewed,=20transaction=20id:=2060455?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/client/src/api/cached-promise.js | 9 +- .../core/extends/merge-render-events.js | 13 +- .../core/extends/set-prop.js | 16 +- .../core/helper/merge-data-to-node.js | 18 +- .../styles/strategy/custom-style.vue | 7 +- lib/server/controller/iam.js | 6 +- lib/server/model/component-category.js | 5 +- lib/server/model/component.js | 5 +- lib/server/model/page-template-category.js | 5 +- lib/server/model/page-template.js | 5 +- lib/shared/constant.js | 1 - lib/shared/page-code/common/modelMethods.js | 10 +- lib/shared/page-code/common/utils.js | 24 +- .../security/property-injection-guard.js | 215 ++++++++++++++++++ 14 files changed, 296 insertions(+), 43 deletions(-) create mode 100644 lib/shared/security/property-injection-guard.js 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/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/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/server/controller/iam.js b/lib/server/controller/iam.js index 9e3eb2b81..5dd3ab9cc 100644 --- a/lib/server/controller/iam.js +++ b/lib/server/controller/iam.js @@ -24,6 +24,7 @@ import { IAM_ACTION, IAM_RESOURCE_TYPE_ID, IAM_APP_PERM_BUILDIN_ACTION } from '. import { logger } from '../logger' import { uuid } from '../util' import { sendReq } from './iam-migration-helper' +import { isSafePropertyKey } from '../../shared/security/property-injection-guard' const iamController = { async serviceCheckAppPerm (ctx) { @@ -504,14 +505,11 @@ const iamController = { } }) - const FORBIDDEN_KEYS = ['__proto__', 'constructor', 'prototype'] - // 只允许字母数字和下划线,且不允许空字符串 - const SAFE_KEY_REGEX = /^[a-zA-Z0-9_-]+$/ // res.data.results 的 length 与参数 actions 的 length 一致,顺序也一致 const ret = {} res.data?.results?.forEach((item, index) => { const key = actions[index]?.actionId - if (typeof key !== 'string' || FORBIDDEN_KEYS.includes(key) || !SAFE_KEY_REGEX?.test(key)) { + if (typeof key !== 'string' || !isSafePropertyKey(key)) { throw new Error(`非法actionId${key}`) } ret[key] = { diff --git a/lib/server/model/component-category.js b/lib/server/model/component-category.js index b4c9851b0..46559568a 100644 --- a/lib/server/model/component-category.js +++ b/lib/server/model/component-category.js @@ -11,6 +11,7 @@ import { getRepository } from 'typeorm' import CompCategory from './entities/comp-category' +import { isSafePropertyKey } from '../../shared/security/property-injection-guard' export const all = async function (params = {}) { const res = await getRepository(CompCategory).find({ @@ -44,7 +45,9 @@ export const updateById = async function (id, params = {}) { throw new Error(global.i18n.t('分类不存在')) } Object.keys(params).forEach(field => { - compCategory[field] = params[field] + if (isSafePropertyKey(field)) { + compCategory[field] = params[field] + } }) const res = await getRepository(CompCategory).save(compCategory) return res diff --git a/lib/server/model/component.js b/lib/server/model/component.js index e4a22f56a..f82f8dcf3 100644 --- a/lib/server/model/component.js +++ b/lib/server/model/component.js @@ -14,6 +14,7 @@ import Project from './entities/project' import Comp from './entities/comp' import CompCategory from './entities/comp-category' import Version from './entities/version' +import { isSafePropertyKey } from '../../shared/security/property-injection-guard' export const all = async function (params = {}, condition = [], conParams = {}, orderKey = '') { orderKey = orderKey || 'createTime' @@ -135,7 +136,9 @@ export const updateById = async function (id, params = {}) { throw new Error(global.i18n.t('组件不存在')) } Object.keys(params).forEach(field => { - comp[field] = params[field] + if (isSafePropertyKey(field)) { + comp[field] = params[field] + } }) const res = await getRepository(Comp).save(comp) return res diff --git a/lib/server/model/page-template-category.js b/lib/server/model/page-template-category.js index e7ba652cb..a1b4e68a1 100644 --- a/lib/server/model/page-template-category.js +++ b/lib/server/model/page-template-category.js @@ -11,6 +11,7 @@ import { getRepository } from 'typeorm' import PageTemplateCategory from './entities/page-template-category' +import { isSafePropertyKey } from '../../shared/security/property-injection-guard' export const all = async function (params = {}) { const res = await getRepository(PageTemplateCategory).find({ @@ -44,7 +45,9 @@ export const updateById = async function (id, params = {}) { throw new Error(global.i18n.t('分类不存在')) } Object.keys(params).forEach(field => { - category[field] = params[field] + if (isSafePropertyKey(field)) { + category[field] = params[field] + } }) const res = await getRepository(PageTemplateCategory).save(category) return res diff --git a/lib/server/model/page-template.js b/lib/server/model/page-template.js index 60b0a867c..ac73ade48 100644 --- a/lib/server/model/page-template.js +++ b/lib/server/model/page-template.js @@ -11,6 +11,7 @@ import { getRepository } from 'typeorm' import PageTemplate from './entities/page-template' +import { isSafePropertyKey } from '../../shared/security/property-injection-guard' export const all = async function (params = {}) { const res = await getRepository(PageTemplate) @@ -53,7 +54,9 @@ export const updateById = async function (id, params = {}) { throw new Error(global.i18n.t('模板不存在')) } Object.keys(params).forEach(field => { - template[field] = params[field] + if (isSafePropertyKey(field)) { + template[field] = params[field] + } }) const res = await getRepository(PageTemplate).save(template) return res 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 fded43a40..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 需要判读的值 @@ -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/security/property-injection-guard.js b/lib/shared/security/property-injection-guard.js new file mode 100644 index 000000000..f3aa1f8dc --- /dev/null +++ b/lib/shared/security/property-injection-guard.js @@ -0,0 +1,215 @@ +/** + * @desc 防止Remote property injection攻击的安全工具函数 + * @author LessCode Security Team + */ + +// 禁止的属性名,这些属性可能被用于原型链污染攻击 +const FORBIDDEN_KEYS = [ + '__proto__', + 'constructor', + 'prototype', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'toLocaleString', + 'toString', + 'valueOf' +] + +// 安全的属性名正则表达式,只允许字母、数字、下划线和连字符 +const SAFE_KEY_REGEX = /^[a-zA-Z0-9_-]+$/ + +/** + * @desc 检查属性名是否安全 + * @param {string} key 属性名 + * @returns {boolean} 是否安全 + */ +export function isSafePropertyKey (key) { + if (typeof key !== 'string') { + return false + } + + // 检查是否在禁止列表中 + if (FORBIDDEN_KEYS.includes(key)) { + return false + } + + // 检查是否符合安全命名规范 + if (!SAFE_KEY_REGEX.test(key)) { + return false + } + + return true +} + +/** + * @desc 安全地合并对象属性,防止原型链污染 + * @param {Object} target 目标对象 + * @param {Object} source 源对象 + * @param {Object} options 选项 + * @param {boolean} options.deep 是否深度合并 + * @param {boolean} options.strict 是否严格模式(拒绝所有不安全的属性) + * @returns {Object} 合并后的安全对象 + */ +export function safeObjectMerge (target = {}, source = {}, options = {}) { + const { deep = false, strict = true } = options + + // 创建没有原型链的安全对象 + const safeTarget = Object.create(null) + + // 先复制目标对象的属性 + if (target && typeof target === 'object') { + for (const key in target) { + if (Object.prototype.hasOwnProperty.call(target, key) && isSafePropertyKey(key)) { + safeTarget[key] = deep ? deepClone(target[key]) : target[key] + } + } + } + + // 再合并源对象的属性 + if (source && typeof source === 'object') { + for (const key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + if (isSafePropertyKey(key)) { + if (deep && typeof source[key] === 'object' && source[key] !== null) { + safeTarget[key] = safeObjectMerge(safeTarget[key] || {}, source[key], { deep: true, strict }) + } else { + safeTarget[key] = source[key] + } + } else if (strict) { + // 严格模式下,记录不安全的属性但拒绝合并 + console.warn(`[Security] Rejected unsafe property key: ${key}`) + } + } + } + } + + return safeTarget +} + +/** + * @desc 安全地设置对象属性 + * @param {Object} target 目标对象 + * @param {string} key 属性名 + * @param {any} value 属性值 + * @param {boolean} strict 是否严格模式 + * @returns {boolean} 是否设置成功 + */ +export function safeSetProperty (target, key, value, strict = true) { + if (!target || typeof target !== 'object') { + return false + } + + if (!isSafePropertyKey(key)) { + if (strict) { + console.warn(`[Security] Rejected unsafe property key: ${key}`) + return false + } + return false + } + + target[key] = value + return true +} + +/** + * @desc 安全地遍历对象属性 + * @param {Object} obj 要遍历的对象 + * @param {Function} callback 回调函数 + * @param {boolean} strict 是否严格模式 + */ +export function safeForEachProperty (obj, callback, strict = true) { + if (!obj || typeof obj !== 'object') { + return + } + + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + if (isSafePropertyKey(key)) { + callback(key, obj[key]) + } else if (strict) { + console.warn(`[Security] Skipped unsafe property key: ${key}`) + } + } + } +} + +/** + * @desc 深度克隆对象,防止原型链污染 + * @param {any} obj 要克隆的对象 + * @returns {any} 克隆后的对象 + */ +export function deepClone (obj) { + if (obj === null || typeof obj !== 'object') { + return obj + } + + if (obj instanceof Date) { + return new Date(obj.getTime()) + } + + if (obj instanceof Array) { + return obj.map(item => deepClone(item)) + } + + if (typeof obj === 'object') { + const cloned = Object.create(null) + safeForEachProperty(obj, (key, value) => { + cloned[key] = deepClone(value) + }, false) // 深度克隆时不严格,但会过滤不安全的key + return cloned + } + + return obj +} + +/** + * @desc 验证并清理对象,移除不安全的属性 + * @param {Object} obj 要清理的对象 + * @param {boolean} deep 是否深度清理 + * @returns {Object} 清理后的安全对象 + */ +export function sanitizeObject (obj, deep = false) { + if (!obj || typeof obj !== 'object') { + return obj + } + + const sanitized = Object.create(null) + + safeForEachProperty(obj, (key, value) => { + if (deep && typeof value === 'object' && value !== null) { + sanitized[key] = sanitizeObject(value, true) + } else { + sanitized[key] = value + } + }, false) // 清理时不严格,但会过滤不安全的key + + return sanitized +} + +/** + * @desc 检查对象是否包含不安全的属性 + * @param {Object} obj 要检查的对象 + * @param {boolean} deep 是否深度检查 + * @returns {Array} 不安全属性的列表 + */ +export function findUnsafeProperties (obj, deep = false) { + const unsafeProperties = [] + + if (!obj || typeof obj !== 'object') { + return unsafeProperties + } + + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + if (!isSafePropertyKey(key)) { + unsafeProperties.push(key) + } else if (deep && typeof obj[key] === 'object' && obj[key] !== null) { + const nestedUnsafe = findUnsafeProperties(obj[key], true) + unsafeProperties.push(...nestedUnsafe.map(nestedKey => `${key}.${nestedKey}`)) + } + } + } + + return unsafeProperties +} From a1eabbfeb03fff4ba60c74cb306074b663e8cb0a Mon Sep 17 00:00:00 2001 From: LivySara <1936808975@qq.com> Date: Thu, 13 Nov 2025 10:33:54 +0800 Subject: [PATCH 11/12] =?UTF-8?q?feat:=20xss=E9=98=B2=E6=8A=A4=20#=20Revie?= =?UTF-8?q?wed,=20transaction=20id:=2064339?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/client/src/common/util.js | 10 ++++--- .../src/components/code-viewer/index.vue | 10 ++----- .../methods/forms/form-items/market.vue | 6 ++++- .../materials/vue2/bk/paragraph/index.js | 18 ++++++------- .../materials/vue2/vant/paragraph/index.js | 18 ++++++------- .../materials/vue3/bk/paragraph/index.js | 18 ++++++------- .../materials/vue3/vant/paragraph/index.js | 18 ++++++------- .../props/components/render-prop.vue | 8 +++--- lib/client/src/main.js | 2 ++ lib/client/src/preview/overlay.js | 4 +-- .../page-manage/children/page-menu-item.vue | 10 ++----- .../components/render-template.vue | 20 +++----------- .../components/template-import-dialog.vue | 3 ++- .../views/system/components/project-form.vue | 27 ++++++++++--------- .../src/views/system/template-market.vue | 11 ++------ package.json | 1 + 16 files changed, 82 insertions(+), 102 deletions(-) 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/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/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/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/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/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 @@