-
+
{{$t('已耗时:')}}
diff --git a/lib/client/src/views/project/release/publish-mixin.js b/lib/client/src/views/project/release/publish-mixin.js
index 16e2f2a75..e2f9bf82b 100644
--- a/lib/client/src/views/project/release/publish-mixin.js
+++ b/lib/client/src/views/project/release/publish-mixin.js
@@ -1,4 +1,5 @@
import dayjs from 'dayjs'
+import { safeJsonParse } from 'shared/security/xss-protection'
export default {
data () {
@@ -16,7 +17,7 @@ export default {
let time = info.createTime ? dayjs(info.createTime).format('YYYY-MM-DD HH:mm:ss') : '--'
let updateUser = info.updateUser
if (info.releaseType === 'FROM_V3') {
- const paasInfo = info.fromPaasInfo ? JSON.parse(info.fromPaasInfo) : {}
+ const paasInfo = safeJsonParse(info.fromPaasInfo, {})
time = paasInfo.updateTime || time
updateUser = paasInfo.updateUser || updateUser
}
diff --git a/lib/client/src/views/project/release/publish.vue b/lib/client/src/views/project/release/publish.vue
index a785a8d15..80acf9c0a 100644
--- a/lib/client/src/views/project/release/publish.vue
+++ b/lib/client/src/views/project/release/publish.vue
@@ -97,7 +97,7 @@
- {{ $t('最近{0}版本:', [typeMap[latestInfo.isOffline]]) }}
+ {{ $t('最近{0}版本:', [typeMap[latestInfo.isOffline]]) }}
,{{ latestInfo.status === 'successful' ? $t('查看成功日志') : (latestInfo.status === 'failed' ? $t('查看失败日志') : $t('查看日志'))}}
diff --git a/lib/client/src/views/system/index.vue b/lib/client/src/views/system/index.vue
index bb0151087..322a5eab1 100644
--- a/lib/client/src/views/system/index.vue
+++ b/lib/client/src/views/system/index.vue
@@ -59,7 +59,7 @@
diff --git a/lib/client/src/views/system/operation/stats/export-button.vue b/lib/client/src/views/system/operation/stats/export-button.vue
index 2737c13a5..d0bc57d27 100644
--- a/lib/client/src/views/system/operation/stats/export-button.vue
+++ b/lib/client/src/views/system/operation/stats/export-button.vue
@@ -3,7 +3,7 @@
>
- import * as XLSX from 'xlsx'
+ import { downloadXlsxFromAoa } from 'shared/excel'
export default {
props: {
@@ -89,9 +89,9 @@
})
const data = [header, ...body]
- this.generateXlsx(`lesscode-stats-${this.name}.xlsx`, data, this.tableSheetName)
+ await this.generateXlsx(`lesscode-stats-${this.name}.xlsx`, data, this.tableSheetName)
},
- handleCommonTimeDimExport () {
+ async handleCommonTimeDimExport () {
const blocks = []
const max = this.list.length
this.list.forEach((block, index) => {
@@ -114,13 +114,10 @@
blocks.push([])
}
})
- this.generateXlsx(`lesscode-stats-${this.name}-${this.dim}.xlsx`, blocks, window.i18n.t('按时间'))
+ await this.generateXlsx(`lesscode-stats-${this.name}-${this.dim}.xlsx`, blocks, window.i18n.t('按时间'))
},
generateXlsx (fileName, data, sheetName) {
- const wb = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet(data)
- XLSX.utils.book_append_sheet(wb, ws, sheetName)
- XLSX.writeFile(wb, fileName)
+ return downloadXlsxFromAoa(fileName, sheetName, data)
},
fistLetterUpper (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
diff --git a/lib/server/app.browser.js b/lib/server/app.browser.js
index 8a93f531b..be53332ca 100644
--- a/lib/server/app.browser.js
+++ b/lib/server/app.browser.js
@@ -265,7 +265,7 @@ async function startServer () {
app.use(convert.compose(allowedMethods))
app.context.render = views(resolve(__dirname, '..', IS_DEV ? 'client' : 'client/dist'), {
- map: { html: 'swig' }
+ map: { html: 'ejs' }
})
app.use(historyApiFallback({
diff --git a/lib/server/controller/data-source.js b/lib/server/controller/data-source.js
index 282ef4c2e..3bf190771 100644
--- a/lib/server/controller/data-source.js
+++ b/lib/server/controller/data-source.js
@@ -724,7 +724,7 @@ export default class DataSourceController {
})
const fileName = fileType === 'sql' ? `lesscode-struct-${projectId}.sql` : ''
const zipName = `lesscode-struct-${projectId}`
- const fileList = generateExportStruct(tables, fileType, fileName)
+ const fileList = await generateExportStruct(tables, fileType, fileName)
return { fileList, zipName }
}
@@ -770,7 +770,7 @@ export default class DataSourceController {
const fileName = fileType === DATA_FILE_TYPE.SQL ? `lesscode-data-${projectId}.sql` : ''
const zipName = `lesscode-data-${projectId}`
- const fileList = generateExportDatas(datas, fileType, fileName)
+ const fileList = await generateExportDatas(datas, fileType, fileName)
return { fileList, zipName }
}
}
diff --git a/lib/server/controller/open-api.js b/lib/server/controller/open-api.js
index d5b5dda10..7f5535b98 100644
--- a/lib/server/controller/open-api.js
+++ b/lib/server/controller/open-api.js
@@ -17,6 +17,7 @@ import { myProject } from './iam'
import { getPreviewDbConfig, getTables, getTableDetail } from '../service/business/preview-db-service'
import DBEngineService from '../service/common/db-engine-service'
+import { logger } from '../logger'
const { createDemoProject } = require('./project')
@@ -260,25 +261,132 @@ export const getProjectTableCols = async (ctx) => {
}
}
+/**
+ * 增强的SQL安全检查函数
+ * @param {string} sql SQL语句
+ * @throws {Error} 如果检测到危险模式
+ */
+const enhancedSqlSecurityCheck = (sql) => {
+ if (!sql || typeof sql !== 'string') {
+ throw new Error('SQL语句不能为空')
+ }
+
+ const trimmedSql = sql.trim()
+ const upperCaseSql = trimmedSql.toUpperCase()
+
+ // 基本检查:必须以SELECT开头
+ if (!upperCaseSql.startsWith('SELECT')) {
+ throw new Error('仅支持SELECT查询语句')
+ }
+
+ // 危险关键词检查(更全面)
+ const dangerousKeywords = [
+ 'DROP DATABASE', 'TRUNCATE TABLE', 'DROP TABLE', 'CREATE TABLE',
+ 'DELETE FROM', 'ALTER TABLE', 'INSERT INTO', 'UPDATE SET',
+ 'GRANT', 'REVOKE', 'CREATE USER', 'DROP USER', 'SET PASSWORD',
+ 'LOAD DATA', 'OUTFILE', 'DUMPFILE', 'LOAD_FILE'
+ ]
+
+ for (const keyword of dangerousKeywords) {
+ if (upperCaseSql.includes(keyword)) {
+ throw new Error(`检测到危险关键词: ${keyword}`)
+ }
+ }
+
+ // 危险模式检查
+ const dangerousPatterns = [
+ /UNION\s+SELECT/i, // UNION注入
+ /;\s*(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|GRANT|REVOKE)/i, // 多语句注入
+ /--/, // SQL注释
+ /\/\*/, // 多行注释开始
+ /\*\//, // 多行注释结束
+ /XP_CMDSHELL/i, // SQL Server命令执行
+ /SP_EXECUTESQL/i, // SQL Server动态SQL
+ /EXEC\s*\(/i, // 执行函数
+ /EXECUTE\s*\(/i, // 执行函数
+ /INFORMATION_SCHEMA/i, // 信息模式
+ /PERFORMANCE_SCHEMA/i, // 性能模式
+ /MYSQL\./i, // MySQL系统表
+ /\@\@/, // 系统变量
+ /DATABASE\(\)/i, // 数据库函数
+ /USER\(\)/i, // 用户函数
+ /VERSION\(\)/i, // 版本函数
+ /BENCHMARK\s*\(/i, // 基准测试函数
+ /SLEEP\s*\(/i, // 延时函数
+ /WAITFOR\s+DELAY/i, // SQL Server延时
+ /PG_SLEEP\s*\(/i, // PostgreSQL延时
+ /DBMS_PIPE\.RECEIVE_MESSAGE/i, // Oracle延时
+ /INTO\s+OUTFILE/i, // 文件写入
+ /INTO\s+DUMPFILE/i, // 文件转储
+ /LOAD_FILE\s*\(/i, // 文件读取
+ /CHAR\s*\(/i, // 字符编码绕过
+ /ASCII\s*\(/i, // ASCII编码绕过
+ /HEX\s*\(/i, // 十六进制编码绕过
+ /UNHEX\s*\(/i, // 反十六进制编码
+ /CONCAT\s*\(/i, // 字符串拼接(可能用于绕过)
+ /SUBSTRING\s*\(/i, // 子字符串(可能用于盲注)
+ /MID\s*\(/i, // 中间字符串
+ /LEFT\s*\(/i, // 左侧字符串
+ /RIGHT\s*\(/i, // 右侧字符串
+ /IF\s*\(/i, // 条件函数(可能用于盲注)
+ /CASE\s+WHEN/i, // CASE语句(可能用于盲注)
+ /EXTRACTVALUE\s*\(/i, // XML函数(可能用于报错注入)
+ /UPDATEXML\s*\(/i, // XML更新函数
+ /NAME_CONST\s*\(/i, // 名称常量函数
+ /MULTIPOINT\s*\(/i, // 几何函数
+ /POLYGON\s*\(/i, // 多边形函数
+ /MULTIPOLYGON\s*\(/i, // 多多边形函数
+ /LINESTRING\s*\(/i, // 线字符串函数
+ /MULTILINESTRING\s*\(/i // 多线字符串函数
+ ]
+
+ for (const pattern of dangerousPatterns) {
+ if (pattern.test(sql)) {
+ throw new Error(`检测到危险的SQL模式: ${pattern.source}`)
+ }
+ }
+}
+
// 执行sql查询,获取预览环境用户db表下数据(当前仅会开放给bk-vision)
export const execQuerySql = async (ctx) => {
- // 涉及到表变更sql的关键词
- const sqlKeywords = ['DROP DATABASE', 'TRRUNCATE TABLE', 'DROP TABLE', 'CREATE TABLE', 'DELETE FROM', 'ALTER TABLE', 'INSERT INTO']
const post = ctx.request.body
const { projectId, sql } = post
- const upperCaseSql = sql && sql.toUpperCase()
- // 此接口只能执行查询语句,禁止执行任何涉及表变更的语句
- if (!upperCaseSql || !upperCaseSql.startsWith('SELECT') || sqlKeywords.indexOf(upperCaseSql) !== -1) {
- ctx.throw(400, 'sql语句未以SELECT开头或含有变更数据表的危险关键词', { code: 400 })
+ // 参数验证
+ if (!projectId) {
+ ctx.throw(400, '缺少必要参数 projectId', { code: 400 })
+ }
+
+ if (!sql || typeof sql !== 'string') {
+ ctx.throw(400, 'SQL语句不能为空且必须为字符串类型', { code: 400 })
}
try {
+ // 增强的安全检查
+ enhancedSqlSecurityCheck(sql)
+
const previewDbConfig = await getPreviewDbConfig(projectId)
const dbEngine = new DBEngineService(previewDbConfig)
const res = await dbEngine.execSql(sql)
success(ctx, res)
} catch (err) {
- ctx.throw(500, err.sqlMessage || err.message || err, { code: 500 })
+ // 记录详细的错误日志
+ logger.error({
+ message: 'SQL查询执行失败',
+ projectId,
+ sql: sql.substring(0, 200), // 只记录前200个字符,避免日志过长
+ error: err.message,
+ stack: err.stack,
+ timestamp: new Date().toISOString()
+ })
+
+ // 不暴露具体的SQL错误信息给客户端
+ const safeErrorMessage = err.message.includes('检测到危险') ||
+ err.message.includes('SQL语句') ||
+ err.message.includes('仅支持SELECT')
+ ? err.message
+ : '查询执行失败,请检查SQL语句'
+
+ ctx.throw(400, safeErrorMessage, { code: 400 })
}
}
diff --git a/lib/server/controller/page.js b/lib/server/controller/page.js
index 1832b4713..c4bb924ce 100644
--- a/lib/server/controller/page.js
+++ b/lib/server/controller/page.js
@@ -624,7 +624,7 @@ export const pageLockStatus = async ctx => {
})
} catch (error) {
ctx.throwError({
- message: error
+ message: error?.message || error
})
}
}
@@ -646,7 +646,7 @@ export const updatePageActive = async ctx => {
})
} catch (error) {
ctx.throwError({
- message: error
+ message: error?.message || error
})
}
}
@@ -675,7 +675,7 @@ export const occupyPage = async ctx => {
})
} catch (error) {
ctx.throwError({
- message: error
+ message: error?.message || error
})
}
}
diff --git a/lib/server/decorator/send/index.js b/lib/server/decorator/send/index.js
index 5c55a9571..58fc235e0 100644
--- a/lib/server/decorator/send/index.js
+++ b/lib/server/decorator/send/index.js
@@ -14,6 +14,24 @@ const path = require('path')
const fse = require('fs-extra')
const { logger } = require('../../logger')
+const sanitizePathName = (name = '') => name.replace(/[/\\]/g, '_')
+
+const toWriteBuffer = (content) => {
+ if (Buffer.isBuffer(content)) {
+ return content
+ }
+ if (content instanceof ArrayBuffer) {
+ return Buffer.from(content)
+ }
+ if (ArrayBuffer.isView(content)) {
+ return Buffer.from(content.buffer, content.byteOffset, content.byteLength)
+ }
+ if (typeof content === 'string') {
+ return Buffer.from(content, 'utf8')
+ }
+ return Buffer.from(content)
+}
+
const outputError = (error, ctx) => {
// 结构化日志记录错误
logger.error(error)
@@ -67,8 +85,8 @@ export const OutputZip = () => {
try {
const { fileList } = await originValue.apply(this, [ctx])
let { zipName } = await originValue.apply(this, [ctx])
- // 去除路径名中的./\符号
- zipName = zipName?.replace(/[./\\]/g, '_')
+ // 去除路径分隔符,保留扩展名中的 .
+ zipName = sanitizePathName(zipName || '')
curDownLoadDirPath = path.join(downloadTempPath, zipName)
curDownLoadFilesPath = path.join(curDownLoadDirPath, zipName)
// 确保下载临时目录有
@@ -77,10 +95,9 @@ export const OutputZip = () => {
await fse.ensureDir(curDownLoadFilesPath)
// 生成 文件列表
fileList.forEach((file) => {
- let name = file.name || zipName
- name = name?.replace(/[./\\]/g, '_')
+ const name = sanitizePathName(file.name || zipName)
const filePath = path.join(curDownLoadFilesPath, name)
- fse.writeFileSync(filePath, file.content, 'utf8')
+ fse.writeFileSync(filePath, toWriteBuffer(file.content))
})
// 生成 zip
const curDownLoadZipPath = path.join(curDownLoadDirPath, `${zipName}.zip`)
diff --git a/lib/server/middleware/error.js b/lib/server/middleware/error.js
index 726df0045..91fd191c4 100644
--- a/lib/server/middleware/error.js
+++ b/lib/server/middleware/error.js
@@ -9,6 +9,19 @@
* specific language governing permissions and limitations under the License.
*/
+const formatErrorMessage = (message) => {
+ if (message == null || message === '') {
+ return global.i18n?.t?.('服务器错误') || '服务器错误'
+ }
+ if (typeof message === 'string') {
+ return message
+ }
+ if (message instanceof Error) {
+ return message.message || global.i18n?.t?.('服务器错误') || '服务器错误'
+ }
+ return String(message)
+}
+
module.exports = () => {
return async function (ctx, next) {
/**
@@ -26,7 +39,7 @@ module.exports = () => {
message,
code
} = error
- throw new global.BusinessError(message, code, status)
+ throw new global.BusinessError(formatErrorMessage(message), code, status)
}
}
/**
@@ -50,7 +63,7 @@ module.exports = () => {
ctx.status = status
ctx.body = {
code,
- message,
+ message: formatErrorMessage(message),
data
}
diff --git a/lib/server/model/project-code.js b/lib/server/model/project-code.js
index 5555d7a61..e23a3ad0b 100644
--- a/lib/server/model/project-code.js
+++ b/lib/server/model/project-code.js
@@ -616,10 +616,10 @@ const projectCode = {
await this.writePackageJSON(
path.join(targetPath, 'package.json'),
[
- { name: 'moment', version: '2.29.4' },
+ { name: 'moment', version: '2.30.1' },
{ name: 'animate.css', version: '4.1.1' },
{ name: 'swiper-element-animation', version: '1.1.0' },
- { name: 'swiper', version: '11.0.5' },
+ { name: 'swiper', version: '^12.1.2' },
{ name: 'lucky-canvas', version: '1.7.27' }
]
)
diff --git a/lib/server/project-template/vue2/project-init-code/bk.config.js b/lib/server/project-template/vue2/project-init-code/bk.config.js
index 1850942a6..5a228dfbc 100644
--- a/lib/server/project-template/vue2/project-init-code/bk.config.js
+++ b/lib/server/project-template/vue2/project-init-code/bk.config.js
@@ -47,6 +47,19 @@ module.exports = {
target: serverAddress
}
]
+ },
+ optimization: {
+ splitChunks: {
+ cacheGroups: {
+ exceljs: {
+ name: 'exceljs',
+ test: /[\\/]node_modules[\\/]exceljs[\\/]/,
+ priority: 2,
+ chunks: 'all',
+ reuseExistingChunk: true
+ }
+ }
+ }
}
}
}
diff --git a/lib/server/project-template/vue2/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js b/lib/server/project-template/vue2/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js
index e79262c90..d670e5523 100644
--- a/lib/server/project-template/vue2/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js
+++ b/lib/server/project-template/vue2/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js
@@ -1,4 +1,4 @@
-import * as XLSX from 'xlsx'
+import { downloadXlsxFromAoa } from 'shared/excel'
import './index.postcss'
export default {
@@ -61,10 +61,7 @@ export default {
body.push(row)
})
- const wb = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet([header, ...body])
- XLSX.utils.book_append_sheet(wb, ws)
- XLSX.writeFile(wb, `${this.tableName}.xlsx`)
+ await downloadXlsxFromAoa(`${this.tableName}.xlsx`, 'Sheet1', [header, ...body])
} catch (e) {
console.log(e.message || e)
} finally {
diff --git a/lib/server/project-template/vue2/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue b/lib/server/project-template/vue2/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue
index 37eff329c..2f09af5ad 100644
--- a/lib/server/project-template/vue2/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue
+++ b/lib/server/project-template/vue2/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue
@@ -24,7 +24,7 @@
>
- import * as XLSX from 'xlsx'
+ import { downloadXlsxFromAoa } from 'shared/excel'
export default {
name: 'CustomButtons',
@@ -100,10 +100,7 @@
body.push(row)
})
- const wb = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet([header, ...body])
- XLSX.utils.book_append_sheet(wb, ws)
- XLSX.writeFile(wb, `${this.nodeName || this.tableName}.xlsx`)
+ await downloadXlsxFromAoa(`${this.nodeName || this.tableName}.xlsx`, 'Sheet1', [header, ...body])
} catch (e) {
console.log(e.message || e)
} finally {
diff --git a/lib/server/project-template/vue2/project-init-code/lib/server/app.browser.js b/lib/server/project-template/vue2/project-init-code/lib/server/app.browser.js
index be286d26f..af20e61ea 100644
--- a/lib/server/project-template/vue2/project-init-code/lib/server/app.browser.js
+++ b/lib/server/project-template/vue2/project-init-code/lib/server/app.browser.js
@@ -99,7 +99,7 @@ async function startServer () {
app.use(convert.compose(allowedMethods))
app.context.render = views(resolve(__dirname, '..', IS_DEV ? 'client' : 'client/dist'), {
- map: { html: 'swig' }
+ map: { html: 'ejs' }
})
app.use(historyApiFallback({
diff --git a/lib/server/project-template/vue2/project-init-code/lib/server/service/data-service.js b/lib/server/project-template/vue2/project-init-code/lib/server/service/data-service.js
index c2f2a1aa3..cf6acd9b7 100644
--- a/lib/server/project-template/vue2/project-init-code/lib/server/service/data-service.js
+++ b/lib/server/project-template/vue2/project-init-code/lib/server/service/data-service.js
@@ -249,9 +249,10 @@ export function getDataService (name = 'default', customEntityMap) {
* @param {*} query 查询参数
* @returns 获取数据详情结果
*/
- findOne (tableFileName, query = {}) {
+ async findOne (tableFileName, query = {}) {
const repository = getRepositoryByName(tableFileName)
- return repository.findOne({ where: transformQuery(query) }) || {}
+ const result = await repository.findOne({ where: transformQuery(query) })
+ return result || {}
},
/**
diff --git a/lib/server/project-template/vue2/project-init-code/lib/shared/excel/index.js b/lib/server/project-template/vue2/project-init-code/lib/shared/excel/index.js
new file mode 100644
index 000000000..a63f121c3
--- /dev/null
+++ b/lib/server/project-template/vue2/project-init-code/lib/shared/excel/index.js
@@ -0,0 +1,33 @@
+/**
+ * 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 ExcelJS from 'exceljs'
+
+const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+
+/**
+ * 浏览器端下载 xlsx
+ * @param {string} fileName
+ * @param {string} sheetName
+ * @param {Array
>} rows
+ */
+export async function downloadXlsxFromAoa (fileName, sheetName, rows) {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet(sheetName || 'Sheet1')
+ worksheet.addRows(rows)
+ const buffer = await workbook.xlsx.writeBuffer()
+ const blob = new Blob([buffer], { type: XLSX_MIME })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = fileName
+ link.click()
+ URL.revokeObjectURL(url)
+}
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 a52db45a6..1dfbfca11 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.12.0",
+ "axios": "^1.15.2",
"babel-eslint": "~10.0.3",
"babel-plugin-parameter-decorator": "^1.0.16",
"better-npm-run": "~0.1.1",
@@ -120,6 +120,7 @@
"cheerio": "1.0.0-rc.2",
"chokidar": "~3.2.1",
"co-views": "~2.1.0",
+ "ejs": "^3.1.10",
"cookie": "~0.4.0",
"core-js": "^3.25.5",
"dayjs": "^1.9.3",
@@ -138,7 +139,7 @@
"jsonp": "~0.2.1",
"js-base64": "^3.7.7",
"js-cookie": "3.0.1",
- "koa": "~2.8.2",
+ "koa": "^2.16.4",
"koa-body": "^4.2.0",
"koa-bodyparser": "~4.2.1",
"koa-convert": "~1.2.0",
@@ -156,7 +157,7 @@
"node-request-context": "~1.0.5",
"nodemon": "~1.19.3",
"ora": "~4.0.2",
- "path-to-regexp": "^8.3.0",
+ "path-to-regexp": "^8.4.2",
"postcss": "~8.4.31",
"postcss-import": "^15.0.0",
"postcss-mixins": "^9.0.4",
@@ -169,7 +170,6 @@
"query-string": "^7.1.1",
"reflect-metadata": "^0.1.13",
"shx": "~0.3.2",
- "swig": "~1.4.2",
"mavon-editor": "^2.9.0",
"typeorm": "~0.3.27",
"typescript": "^4.4.3",
@@ -180,7 +180,7 @@
"vue-json-viewer": "^2.2.22",
"vue-router": "~3.1.3",
"vuex": "~3.1.1",
- "xlsx": "^0.18.5"
+ "exceljs": "^4.4.0"
},
"overrides": {
"@babel/runtime": "7.23.9",
diff --git a/lib/server/project-template/vue3/project-init-code/bk.config.js b/lib/server/project-template/vue3/project-init-code/bk.config.js
index 0dd422f14..acd75aaa2 100644
--- a/lib/server/project-template/vue3/project-init-code/bk.config.js
+++ b/lib/server/project-template/vue3/project-init-code/bk.config.js
@@ -48,6 +48,19 @@ module.exports = {
target: serverAddress
}
]
+ },
+ optimization: {
+ splitChunks: {
+ cacheGroups: {
+ exceljs: {
+ name: 'exceljs',
+ test: /[\\/]node_modules[\\/]exceljs[\\/]/,
+ priority: 2,
+ chunks: 'all',
+ reuseExistingChunk: true
+ }
+ }
+ }
}
}
}
diff --git a/lib/server/project-template/vue3/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js
index 147f7d15f..fb8299d8d 100644
--- a/lib/server/project-template/vue3/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js
+++ b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js
@@ -1,5 +1,5 @@
import { h, resolveComponent } from 'vue'
-import * as XLSX from 'xlsx'
+import { downloadXlsxFromAoa } from 'shared/excel'
import './index.postcss'
export default {
@@ -62,10 +62,7 @@ export default {
body.push(row)
})
- const wb = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet([header, ...body])
- XLSX.utils.book_append_sheet(wb, ws)
- XLSX.writeFile(wb, `${this.tableName}.xlsx`)
+ await downloadXlsxFromAoa(`${this.tableName}.xlsx`, 'Sheet1', [header, ...body])
} catch (e) {
console.log(e.message || e)
} finally {
diff --git a/lib/server/project-template/vue3/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue
index 37eff329c..2f09af5ad 100644
--- a/lib/server/project-template/vue3/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue
+++ b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue
@@ -24,7 +24,7 @@
>
- import * as XLSX from 'xlsx'
+ import { downloadXlsxFromAoa } from 'shared/excel'
export default {
name: 'CustomButtons',
@@ -100,10 +100,7 @@
body.push(row)
})
- const wb = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet([header, ...body])
- XLSX.utils.book_append_sheet(wb, ws)
- XLSX.writeFile(wb, `${this.nodeName || this.tableName}.xlsx`)
+ await downloadXlsxFromAoa(`${this.nodeName || this.tableName}.xlsx`, 'Sheet1', [header, ...body])
} catch (e) {
console.log(e.message || e)
} finally {
diff --git a/lib/server/project-template/vue3/project-init-code/lib/client/src/components/form-engine/fields/textarea/index.js b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/form-engine/fields/textarea/index.js
index bce14121d..1d4b91305 100644
--- a/lib/server/project-template/vue3/project-init-code/lib/client/src/components/form-engine/fields/textarea/index.js
+++ b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/form-engine/fields/textarea/index.js
@@ -1,4 +1,5 @@
import { h, resolveComponent } from 'vue'
+import './index.postcss'
export default {
name: 'bkform-engine-textarea',
diff --git a/lib/server/project-template/vue3/project-init-code/lib/client/src/components/form-engine/fields/textarea/index.postcss b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/form-engine/fields/textarea/index.postcss
new file mode 100644
index 000000000..7e3d41d85
--- /dev/null
+++ b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/form-engine/fields/textarea/index.postcss
@@ -0,0 +1,8 @@
+.bkform-engine-field-widget {
+ .bk-lesscode-textarea,
+ .bk-textarea {
+ textarea {
+ flex: 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/server/project-template/vue3/project-init-code/lib/server/app.browser.js b/lib/server/project-template/vue3/project-init-code/lib/server/app.browser.js
index be286d26f..af20e61ea 100644
--- a/lib/server/project-template/vue3/project-init-code/lib/server/app.browser.js
+++ b/lib/server/project-template/vue3/project-init-code/lib/server/app.browser.js
@@ -99,7 +99,7 @@ async function startServer () {
app.use(convert.compose(allowedMethods))
app.context.render = views(resolve(__dirname, '..', IS_DEV ? 'client' : 'client/dist'), {
- map: { html: 'swig' }
+ map: { html: 'ejs' }
})
app.use(historyApiFallback({
diff --git a/lib/server/project-template/vue3/project-init-code/lib/server/service/data-service.js b/lib/server/project-template/vue3/project-init-code/lib/server/service/data-service.js
index c2f2a1aa3..cf6acd9b7 100644
--- a/lib/server/project-template/vue3/project-init-code/lib/server/service/data-service.js
+++ b/lib/server/project-template/vue3/project-init-code/lib/server/service/data-service.js
@@ -249,9 +249,10 @@ export function getDataService (name = 'default', customEntityMap) {
* @param {*} query 查询参数
* @returns 获取数据详情结果
*/
- findOne (tableFileName, query = {}) {
+ async findOne (tableFileName, query = {}) {
const repository = getRepositoryByName(tableFileName)
- return repository.findOne({ where: transformQuery(query) }) || {}
+ const result = await repository.findOne({ where: transformQuery(query) })
+ return result || {}
},
/**
diff --git a/lib/server/project-template/vue3/project-init-code/lib/shared/excel/index.js b/lib/server/project-template/vue3/project-init-code/lib/shared/excel/index.js
new file mode 100644
index 000000000..a63f121c3
--- /dev/null
+++ b/lib/server/project-template/vue3/project-init-code/lib/shared/excel/index.js
@@ -0,0 +1,33 @@
+/**
+ * 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 ExcelJS from 'exceljs'
+
+const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+
+/**
+ * 浏览器端下载 xlsx
+ * @param {string} fileName
+ * @param {string} sheetName
+ * @param {Array>} rows
+ */
+export async function downloadXlsxFromAoa (fileName, sheetName, rows) {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet(sheetName || 'Sheet1')
+ worksheet.addRows(rows)
+ const buffer = await workbook.xlsx.writeBuffer()
+ const blob = new Blob([buffer], { type: XLSX_MIME })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = fileName
+ link.click()
+ URL.revokeObjectURL(url)
+}
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 82a726eb7..6346f3bd8 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.12.0",
+ "axios": "^1.15.2",
"babel-eslint": "~10.0.3",
"babel-plugin-parameter-decorator": "^1.0.16",
"better-npm-run": "~0.1.1",
@@ -119,6 +119,7 @@
"cheerio": "1.0.0-rc.2",
"chokidar": "~3.2.1",
"co-views": "~2.1.0",
+ "ejs": "^3.1.10",
"cookie": "~0.4.0",
"core-js": "^3.25.5",
"dayjs": "^1.9.3",
@@ -136,7 +137,7 @@
"jsonp": "~0.2.1",
"js-base64": "^3.7.7",
"js-cookie": "3.0.1",
- "koa": "~2.8.2",
+ "koa": "^2.16.4",
"koa-body": "^4.2.0",
"koa-bodyparser": "~4.2.1",
"koa-convert": "~1.2.0",
@@ -154,7 +155,7 @@
"node-request-context": "~1.0.5",
"nodemon": "~1.19.3",
"ora": "~4.0.2",
- "path-to-regexp": "^8.3.0",
+ "path-to-regexp": "^8.4.2",
"postcss": "~8.4.31",
"postcss-import": "^15.0.0",
"postcss-mixins": "^9.0.4",
@@ -167,7 +168,6 @@
"query-string": "^7.1.1",
"reflect-metadata": "^0.1.13",
"shx": "~0.3.2",
- "swig": "~1.4.2",
"mavon-editor": "^2.9.0",
"typeorm": "~0.3.27",
"typescript": "^4.4.3",
@@ -178,7 +178,7 @@
"vue-json-viewer": "^2.2.22",
"vue-router": "~4.1.6",
"vuex": "^4.1.0",
- "xlsx": "^0.18.5",
+ "exceljs": "^4.4.0",
"less": "^4.2.0"
},
"overrides": {
diff --git a/lib/server/router/index.js b/lib/server/router/index.js
index 732e3c2fd..b0cf6aa70 100644
--- a/lib/server/router/index.js
+++ b/lib/server/router/index.js
@@ -8,7 +8,6 @@
* 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 httpConf from '../conf/http'
const KoaRouter = require('koa-router')
const fs = require('fs')
const path = require('path')
@@ -23,7 +22,6 @@ function getRenderTemplateName (ctx) {
}
const renderParams = {
STATIC_URL: '',
- BK_API_URL_TMPL: httpConf.apiGateWayUrlTmpl,
BKPAAS_BK_DOMAIN: process.env.BKPAAS_BK_DOMAIN,
BK_SHARED_RES_URL: process.env.BKPAAS_SHARED_RES_URL,
BK_COMPONENT_API_URL: process.env.BK_COMPONENT_API_URL
diff --git a/lib/server/service/business/bk-base.js b/lib/server/service/business/bk-base.js
index 93568ffb2..a60c39858 100644
--- a/lib/server/service/business/bk-base.js
+++ b/lib/server/service/business/bk-base.js
@@ -9,6 +9,7 @@ import {
import {
generateToken
} from './token'
+import { escapeIdentifier } from '../../../shared/security/sql-protection'
const getProjectInfo = async (projectId, bkTicket) => {
const projectInfo = await LCDataService.findOne(
@@ -166,6 +167,20 @@ export const getTables = async (projectId, bkTicket, bkBizId) => {
}
}
+/**
+ * 验证排序方向
+ * @param {string} direction 排序方向
+ * @returns {string} 验证后的排序方向
+ */
+const validateSortDirection = (direction) => {
+ const allowedDirections = ['ASC', 'DESC']
+ const upperDirection = String(direction).toUpperCase()
+ if (!allowedDirections.includes(upperDirection)) {
+ throw new Error('排序方向错误:只支持 ASC 或 DESC')
+ }
+ return upperDirection
+}
+
/**
* 获取有权限的数据
* @param {*} projectId 项目id
@@ -175,43 +190,65 @@ export const getTables = async (projectId, bkTicket, bkBizId) => {
* @returns 表数据
*/
export const getTableDatas = async (projectId, queryData, tableFileName, bkTicket) => {
- // 构造查询 SQL
- const {
- page,
- pageSize,
- bkSortKey,
- bkSortValue,
- bkDataSourceType,
- ...others
- } = queryData
- let sql = `SELECT * FROM \`${tableFileName}\``
- Object
- .keys(others)
- .forEach((key, index) => {
- if (index <= 0) { - sql += ' WHERE' - } else { - sql += ' AND' + try { + // 构造查询 SQL - 使用参数化查询防止SQL注入 + const { + page, + pageSize, + bkSortKey, + bkSortValue, + bkDataSourceType, + ...others + } = queryData + + // 验证表名 + const safeTableName = escapeIdentifier(tableFileName) + let sql = `SELECT * FROM ${safeTableName}` + + Object + .keys(others) + .forEach((key, index) => {
+ if (index <= 0) { + sql += ' WHERE' + } else { + sql += ' AND' + } + sql += ` \`${key}\` LIKE '%${others[key]}%'` + }) + + // 构建ORDER BY子句 + if (bkSortKey && bkSortValue) { + const safeSortKey = escapeIdentifier(bkSortKey) + const safeSortDirection = validateSortDirection(bkSortValue) + sql += ` ORDER BY ${safeSortKey} ${safeSortDirection}` + } + + // 构建LIMIT子句 + if (page !== undefined && pageSize !== undefined) { + const pageNum = parseInt(page, 10) + const pageSizeNum = parseInt(pageSize, 10) + + // 验证分页参数 + if (isNaN(pageNum) || isNaN(pageSizeNum) || pageNum < 0 || pageSizeNum <= 0 || pageSizeNum> 1000) {
+ throw new Error('分页参数错误:页码不能为负数,每页大小必须在1-1000之间')
}
- sql += ` \`${key}\` LIKE '%${others[key]}%'`
- })
- if (bkSortKey && bkSortValue) {
- sql += ` ORDER BY \`${bkSortKey}\` ${bkSortValue}`
- }
- if (page && pageSize) {
- const index = page * pageSize
- sql += ` LIMIT ${index}, ${pageSize}`
- }
+
+ const offset = pageNum * pageSizeNum
+ sql += ` LIMIT ${offset}, ${pageSizeNum}`
+ }
- sql += ';'
- // 执行查询
- const multTableDatas = await execSQL(projectId, sql, bkTicket)
- // 过滤 bk-base 系统内置字段
- const list = (multTableDatas?.[0] || []).map((item) => {
- const { dtEventTimeStamp, localTime, thedate, dtEventTime, ...rest } = item
- return rest
- })
- return {
- list
+ sql += ';'
+ // 执行查询
+ const multTableDatas = await execSQL(projectId, sql, bkTicket)
+ // 过滤 bk-base 系统内置字段
+ const list = (multTableDatas?.[0] || []).map((item) => {
+ const { dtEventTimeStamp, localTime, thedate, dtEventTime, ...rest } = item
+ return rest
+ })
+ return {
+ list
+ }
+ } catch (error) {
+ throw new Error(error.message || error)
}
}
diff --git a/lib/server/service/business/data-source.js b/lib/server/service/business/data-source.js
index 6d6e18021..7b2ae19a4 100644
--- a/lib/server/service/business/data-source.js
+++ b/lib/server/service/business/data-source.js
@@ -108,11 +108,19 @@ export const getSqlByCondition = async (projectId, condition) => {
return generateSqlByCondition(condition, list)
}
-// 只能是查询语句检查
+// 只能是查询语句检查 - 增强版本
export const querySqlCheck = (sql) => {
- if (!/;$/.test(sql.trim())) {
+ if (!sql || typeof sql !== 'string') {
+ throw new Error('SQL语句不能为空')
+ }
+
+ const trimmedSql = sql.trim()
+
+ if (!/;$/.test(trimmedSql)) {
throw new Error(global.i18n.t('Sql 语句不完整,需要是【;】号结尾'))
}
+
+ // 基础检查列表(保持原有逻辑)
const checkList = [
{ check: (val) => !/^select/.test(val), message: global.i18n.t('仅支持 SELECT 查询语句,请修改后再试') },
{ check: (val) => /database\(\)/i.test(val), message: global.i18n.t('不允许出现 database() 函数') },
@@ -123,28 +131,75 @@ export const querySqlCheck = (sql) => {
{ check: (val) => /MYSQL\./i.test(val), message: global.i18n.t('不允许查询 MYSQL 内置表相关数据') },
{ check: (val) => /\@\@/i.test(val), message: global.i18n.t('不允许出现@@符号') }
]
+
+ // 增强的安全检查列表
+ const enhancedCheckList = [
+ { check: (val) => /union\s+select/i.test(val), message: global.i18n.t('不允许使用 UNION SELECT 语句') },
+ { check: (val) => /;\s*(drop|delete|update|insert|alter|create|grant|revoke)/i.test(val), message: global.i18n.t('检测到多语句注入尝试') },
+ { check: (val) => /--/.test(val), message: global.i18n.t('不允许使用 SQL 注释符号 --') },
+ { check: (val) => /\/\*/.test(val), message: global.i18n.t('不允许使用多行注释 /*') },
+ { check: (val) => /\*\//.test(val), message: global.i18n.t('不允许使用多行注释 */') },
+ { check: (val) => /xp_cmdshell/i.test(val), message: global.i18n.t('不允许使用 xp_cmdshell 函数') },
+ { check: (val) => /sp_executesql/i.test(val), message: global.i18n.t('不允许使用 sp_executesql 函数') },
+ { check: (val) => /exec\s*\(/i.test(val), message: global.i18n.t('不允许使用 EXEC 函数') },
+ { check: (val) => /execute\s*\(/i.test(val), message: global.i18n.t('不允许使用 EXECUTE 函数') },
+ { check: (val) => /load_file\s*\(/i.test(val), message: global.i18n.t('不允许使用 LOAD_FILE 函数') },
+ { check: (val) => /into\s+outfile/i.test(val), message: global.i18n.t('不允许使用 INTO OUTFILE 语句') },
+ { check: (val) => /into\s+dumpfile/i.test(val), message: global.i18n.t('不允许使用 INTO DUMPFILE 语句') },
+ { check: (val) => /benchmark\s*\(/i.test(val), message: global.i18n.t('不允许使用 BENCHMARK 函数') },
+ { check: (val) => /sleep\s*\(/i.test(val), message: global.i18n.t('不允许使用 SLEEP 函数') },
+ { check: (val) => /waitfor\s+delay/i.test(val), message: global.i18n.t('不允许使用 WAITFOR DELAY 语句') },
+ { check: (val) => /pg_sleep\s*\(/i.test(val), message: global.i18n.t('不允许使用 PG_SLEEP 函数') },
+ { check: (val) => /extractvalue\s*\(/i.test(val), message: global.i18n.t('不允许使用 EXTRACTVALUE 函数') },
+ { check: (val) => /updatexml\s*\(/i.test(val), message: global.i18n.t('不允许使用 UPDATEXML 函数') },
+ { check: (val) => /name_const\s*\(/i.test(val), message: global.i18n.t('不允许使用 NAME_CONST 函数') },
+ { check: (val) => /multipoint\s*\(/i.test(val), message: global.i18n.t('不允许使用 MULTIPOINT 函数') },
+ { check: (val) => /polygon\s*\(/i.test(val), message: global.i18n.t('不允许使用 POLYGON 函数') },
+ { check: (val) => /multipolygon\s*\(/i.test(val), message: global.i18n.t('不允许使用 MULTIPOLYGON 函数') },
+ { check: (val) => /linestring\s*\(/i.test(val), message: global.i18n.t('不允许使用 LINESTRING 函数') },
+ { check: (val) => /multilinestring\s*\(/i.test(val), message: global.i18n.t('不允许使用 MULTILINESTRING 函数') }
+ ]
+
const sqlArr = splitSql(sql)
sqlArr.forEach((sqlStr) => {
const lowerCaseSql = sqlStr.trim().toLowerCase()
if (lowerCaseSql) {
+ // 执行原有检查
checkList.forEach((checkItem) => {
if (checkItem.check(lowerCaseSql)) {
throw new Error(checkItem.message)
}
})
+
+ // 执行增强检查
+ enhancedCheckList.forEach((checkItem) => {
+ if (checkItem.check(lowerCaseSql)) {
+ throw new Error(checkItem.message)
+ }
+ })
}
})
}
// sql 解码
export const decodeSql = (sql, params) => {
- let decodedSql = decodeBase64(sql)
- const paramKeys = Object.keys(params || {})
- if (paramKeys.length> 0) {
- paramKeys.forEach((paramKey) => {
+ try {
+ let decodedSql = decodeBase64(sql)
+
+ // 验证解码后的SQL
+ if (!decodedSql || typeof decodedSql !== 'string') {
+ throw new Error('无效的SQL语句')
+ }
+
+ const paramKeys = Object.keys(params || {})
+ if (paramKeys.length> 0) {
+ paramKeys.forEach((paramKey) => {
const reg = new RegExp(`\\$\\{${paramKey}\\}`, 'g')
decodedSql = decodedSql.replace(reg, params[paramKey])
- })
+ })
+ }
+ return decodedSql
+ } catch (error) {
+ throw new Error(`SQL解码失败: ${error.message}`)
}
- return decodedSql
}
diff --git a/lib/server/service/business/open-api.js b/lib/server/service/business/open-api.js
index 355d05bb5..b9f3bf12e 100644
--- a/lib/server/service/business/open-api.js
+++ b/lib/server/service/business/open-api.js
@@ -9,6 +9,7 @@ import {
execApiGateWay,
decodeToken
} from '@bkui/apigateway-nodejs-sdk'
+import httpConf from '../../conf/http'
import openApiJson from '../../system-conf/open-api.json'
import {
transformVersionToNum
@@ -106,7 +107,7 @@ export const generateApiGateway = async () => {
// 更新资源文档
await execApiGateWay({
apiName: 'bk-apigateway',
- apiUrlTemp: process.env.BK_API_URL_TMPL,
+ apiUrlTemp: httpConf.apiGateWayUrlTmpl,
path: `/api/v1/apis/${apiName}/resource-docs/import/by-archive/`,
method: 'post',
stageName: 'prod',
diff --git a/lib/server/service/business/preview-db-service.js b/lib/server/service/business/preview-db-service.js
index da2764d63..f307f1844 100644
--- a/lib/server/service/business/preview-db-service.js
+++ b/lib/server/service/business/preview-db-service.js
@@ -10,7 +10,7 @@
*/
import DBEngineService from '../common/db-engine-service'
-import { LCDataService, TABLE_FILE_NAME, getDataService } from '../common/data-service'
+import { LCDataService, TABLE_FILE_NAME, getDataService } from '../common/data-service'
import { EntitySchema, createConnection, EventSubscriber, Like } from 'typeorm'
import { RequestContext } from '../../middleware/request-context'
import { sm4Encrypt, sm4Decrypt, uuid } from '../../util'
@@ -19,16 +19,34 @@ import OnlineDBService from '../common/online-db-service'
import {
transferTimeByTimezoneOffset
} from './data-source'
+import { escapeIdentifier } from '../../../shared/security/sql-protection'
+const mysql = require('mysql2')
const dataBaseConf = require('../../conf/data-source')
/**
* 开启预览
* @param {*} projectId 应用id
+ * @param {*} dbName 数据库名称
*/
export const enablePerviewDb = async (projectId, dbName) => {
+ // 输入验证
+ if (!dbName || typeof dbName !== 'string') {
+ throw new Error('数据库名称必须是非空字符串')
+ }
+
+ // 标识符格式校验
+ try {
+ escapeIdentifier(dbName)
+ } catch (error) {
+ throw new Error('数据库名称格式错误:只允许字母、数字、下划线和连字符')
+ }
+
+ // 安全的数据库名称
+ const safeDbName = `bklesscode_${dbName}`
+
const dbInfo = {
projectId,
- dbName: `bklesscode_${dbName}`,
+ dbName: safeDbName,
userName: uuid(),
passWord: uuid()
}
@@ -36,12 +54,20 @@ export const enablePerviewDb = async (projectId, dbName) => {
// 创建用于预览的DB
const previewDbEngine = await getPreviewDbEngine()
await previewDbEngine.execCb(async (pool) => {
+ // 使用 mysql.escapeId 对数据库名进行安全转义
+ const escapedDbName = mysql.escapeId(dbInfo.dbName)
+
// 创建应用对应的预览数据库
- await pool.query(`CREATE DATABASE \`${dbInfo.dbName}\`;`)
- // 创建用户并授权对应的库
- await pool.query(`CREATE USER '${dbInfo.userName}'@'%' IDENTIFIED BY '${dbInfo.passWord}';`)
- await pool.query(`GRANT ALL ON ${dbInfo.dbName}.* TO '${dbInfo.userName}'@'%';`)
- await pool.query('FLUSH PRIVILEGES;')
+ await pool.query(`CREATE DATABASE ${escapedDbName}`)
+
+ // 创建用户并授权对应的库 - 使用 mysql.escape 转义所有值
+ const escapedUserName = mysql.escape(dbInfo.userName)
+ const escapedPassword = mysql.escape(dbInfo.passWord)
+ const escapedHost = mysql.escape('%')
+
+ await pool.query(`CREATE USER ${escapedUserName}@${escapedHost} IDENTIFIED BY ${escapedPassword}`)
+ await pool.query(`GRANT ALL ON ${escapedDbName}.* TO ${escapedUserName}@${escapedHost}`)
+ await pool.query('FLUSH PRIVILEGES')
})
// 加密
@@ -317,7 +343,7 @@ export const getThirdPartDBTables = async (projectId, page, pageSize, thirdPartD
BASE_COLUMNS()[0]
])
})
- updateSql += `ALTER TABLE \`${tableData.tableName}\` ADD COLUMN \`id\` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY;`
+ updateSql += `ALTER TABLE ${mysql.escapeId(tableData.tableName)} ADD COLUMN ${mysql.escapeId('id')} int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY;`
}
}
})
diff --git a/lib/server/service/business/v3-service.js b/lib/server/service/business/v3-service.js
index e38e9c991..b6522e9b7 100644
--- a/lib/server/service/business/v3-service.js
+++ b/lib/server/service/business/v3-service.js
@@ -228,8 +228,7 @@ export const uploadToBkRepo = async (projectId, version, versionId, releaseId) =
return new Promise(async (resolve, reject) => {
try {
shell.cd(STATIC_URL)
- const targz = `tar zcf bklesscode-proj-${projectId}.tar.gz -C ./${sourceDir} .`
- shell.exec(targz)
+ shell.cmd('tar', ['zcf', `bklesscode-proj-${projectId}.tar.gz`, '-C', `./${sourceDir}`, '.'])
shell.cd(projectPath)
// 根据压缩包大小计算md5值
diff --git a/lib/server/service/common/data-service.js b/lib/server/service/common/data-service.js
index 03e8d10ab..9d5654ed9 100644
--- a/lib/server/service/common/data-service.js
+++ b/lib/server/service/common/data-service.js
@@ -312,9 +312,10 @@ export function getDataService (name = 'default', customEntityMap) {
* @param {*} query 查询参数
* @returns 获取数据详情结果
*/
- findOne (tableFileName, query = { deleteFlag: 0 }) {
+ async findOne (tableFileName, query = { deleteFlag: 0 }) {
const repository = getRepositoryByName(tableFileName)
- return repository.findOne({ where: transformQuery(query) }) || {}
+ const result = await repository.findOne({ where: transformQuery(query) })
+ return result || {}
},
/**
* 添加
diff --git a/lib/shared/data-source/data-parse/data-parser/xlsx-parser.js b/lib/shared/data-source/data-parse/data-parser/xlsx-parser.js
index 9c7596489..67b78c221 100644
--- a/lib/shared/data-source/data-parse/data-parser/xlsx-parser.js
+++ b/lib/shared/data-source/data-parse/data-parser/xlsx-parser.js
@@ -8,7 +8,10 @@
* 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 * as XLSX from 'xlsx'
+import {
+ buildXlsxBufferFromAoa,
+ parseFirstSheetToJson
+} from '../../../excel'
import { getTypeByValue } from '../../../util'
import { DATA_TYPES } from '../../../constant'
@@ -17,20 +20,18 @@ import { DATA_TYPES } from '../../../constant'
* @param {*} xlsxs xlsx 文件
* @returns 数据 json
*/
-function transformXlsx2Json (xlsxs) {
- return xlsxs.map(({ content }) => {
- const workBook = XLSX.read(content, { type: 'binary', cellDates: true })
- // 读取第一个 sheet
- return XLSX.utils.sheet_to_json(workBook.Sheets[workBook.SheetNames[0]], { raw: false })
- })
+async function transformXlsx2Json (xlsxs) {
+ const result = []
+ for (const { content } of xlsxs) {
+ result.push(await parseFirstSheetToJson(content, { raw: false, cellDates: true }))
+ }
+ return result
}
-function transformJson2Xlsx (finalDatas) {
- return finalDatas.map(({ tableName, list }) => {
- // 生成表头
+async function transformJson2Xlsx (finalDatas) {
+ const result = []
+ for (const { tableName, list } of finalDatas) {
const header = Object.keys(list[0])
-
- // 生成字段值
const body = []
list.forEach((data) => {
const dataValues = header.map((key) => {
@@ -42,15 +43,10 @@ function transformJson2Xlsx (finalDatas) {
})
body.push(dataValues)
})
-
- // 构造 xlsx 文件
- const workBook = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet([header, ...body])
- XLSX.utils.book_append_sheet(workBook, ws, tableName)
- const content = XLSX.write(workBook, { bookType: 'xlsx', bookSST: false, type: 'buffer' })
-
- return { tableName, content }
- })
+ const content = await buildXlsxBufferFromAoa(tableName, [header, ...body])
+ result.push({ tableName, content })
+ }
+ return result
}
/**
@@ -61,12 +57,12 @@ export class DataXlsxParser {
this.xlsxs = xlsxs
}
- set (that = {}) {
- that.finalDatas = transformXlsx2Json(this.xlsxs)
+ async set (that = {}) {
+ that.finalDatas = await transformXlsx2Json(this.xlsxs)
return that
}
- export (that) {
+ async export (that) {
return transformJson2Xlsx(that.finalDatas)
}
}
diff --git a/lib/shared/data-source/data-parse/index.js b/lib/shared/data-source/data-parse/index.js
index 8aa19e9c6..e9abf7cd4 100644
--- a/lib/shared/data-source/data-parse/index.js
+++ b/lib/shared/data-source/data-parse/index.js
@@ -49,7 +49,7 @@ export class DataParse {
* @param {*} parser 具体执行导出的实例
* @returns 返回导出结果
*/
- export (parser) {
+ async export (parser) {
return parser.export(this)
}
}
diff --git a/lib/shared/data-source/data-parse/struct-parser/xlsx-parser.js b/lib/shared/data-source/data-parse/struct-parser/xlsx-parser.js
index a7c15d627..6a4d5e48d 100644
--- a/lib/shared/data-source/data-parse/struct-parser/xlsx-parser.js
+++ b/lib/shared/data-source/data-parse/struct-parser/xlsx-parser.js
@@ -8,7 +8,10 @@
* 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 * as XLSX from 'xlsx'
+import {
+ buildXlsxBufferFromAoa,
+ parseFirstSheetToJson
+} from '../../../excel'
import {
ORM_KEYS
} from '../../constant'
@@ -18,16 +21,16 @@ import {
* @param {[{ tableName, content }]} xlsxs xlsx 数据
* @returns [{ tableName: 表名, columns: 列信息 }]
*/
-function transformXlsx2Json (xlsxs) {
- return xlsxs.map(({ tableName, content }) => {
- const workBook = XLSX.read(content, { type: 'binary', cellDates: true })
- // 读取第一个 sheet
- const columns = XLSX.utils.sheet_to_json(workBook.Sheets[workBook.SheetNames[0]])
- return {
+async function transformXlsx2Json (xlsxs) {
+ const result = []
+ for (const { tableName, content } of xlsxs) {
+ const columns = await parseFirstSheetToJson(content, { cellDates: true })
+ result.push({
tableName,
columns
- }
- })
+ })
+ }
+ return result
}
/**
@@ -35,12 +38,10 @@ function transformXlsx2Json (xlsxs) {
* @param {*} finalDatas table json
* @returns [{ tableName: 表名, content: 文件内容 }]
*/
-function transformJson2Xlsx (finalDatas) {
- return finalDatas.map(({ tableName, columns }) => {
- // 生成表头
+async function transformJson2Xlsx (finalDatas) {
+ const result = []
+ for (const { tableName, columns } of finalDatas) {
const header = ORM_KEYS.filter(x => x !== 'columnId')
-
- // 生成字段值
const body = []
columns.forEach((column) => {
const columnValue = header.map((key) => {
@@ -48,15 +49,10 @@ function transformJson2Xlsx (finalDatas) {
})
body.push(columnValue)
})
-
- // 构造 xlsx 文件
- const workBook = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet([header, ...body])
- XLSX.utils.book_append_sheet(workBook, ws, tableName)
- const content = XLSX.write(workBook, { bookType: 'xlsx', bookSST: false, type: 'buffer' })
-
- return { tableName, content }
- })
+ const content = await buildXlsxBufferFromAoa(tableName, [header, ...body])
+ result.push({ tableName, content })
+ }
+ return result
}
/**
@@ -67,12 +63,12 @@ export class StructXlsxParser {
this.xlsxs = xlsxs
}
- set (that = {}) {
- that.finalDatas = transformXlsx2Json(this.xlsxs)
+ async set (that = {}) {
+ that.finalDatas = await transformXlsx2Json(this.xlsxs)
return that
}
- export (that) {
+ async export (that) {
return transformJson2Xlsx(that.finalDatas)
}
}
diff --git a/lib/shared/data-source/helper.js b/lib/shared/data-source/helper.js
index 889879669..8ba6349dd 100644
--- a/lib/shared/data-source/helper.js
+++ b/lib/shared/data-source/helper.js
@@ -45,16 +45,16 @@ import {
* @param {*} name 导出文件名
* @returns 返回导出内容 [{ name, conent }]
*/
-export const generateExportStruct = (tables, fileType, name) => {
+export const generateExportStruct = async (tables, fileType, name) => {
const dataParse = new DataParse()
const structJsonParser = new StructJsonParser(tables)
if (fileType === DATA_FILE_TYPE.SQL) {
const structSqlParser = new StructSqlParser()
- const content = dataParse.import(structJsonParser).export(structSqlParser)
+ const content = await dataParse.import(structJsonParser).export(structSqlParser)
return [{ content, name }]
} else {
const structXlsxParser = new StructXlsxParser()
- const fileContents = dataParse.import(structJsonParser).export(structXlsxParser)
+ const fileContents = await dataParse.import(structJsonParser).export(structXlsxParser)
return fileContents.map(({ tableName, content }) => {
return {
name: name || `${tableName}.xlsx`,
@@ -71,16 +71,16 @@ export const generateExportStruct = (tables, fileType, name) => {
* @param {*} name 导出文件名
* @returns 返回导出内容 [{ name, conent }]
*/
-export const generateExportDatas = (datas, fileType, name) => {
+export const generateExportDatas = async (datas, fileType, name) => {
const dataParse = new DataParse()
const dataJsonParser = new DataJsonParser(datas)
if (fileType === DATA_FILE_TYPE.SQL) {
const dataSqlParser = new DataSqlParser()
- const content = dataParse.import(dataJsonParser).export(dataSqlParser)
+ const content = await dataParse.import(dataJsonParser).export(dataSqlParser)
return [{ content, name }]
} else {
const dataXlsxParser = new DataXlsxParser()
- const fileContents = dataParse.import(dataJsonParser).export(dataXlsxParser)
+ const fileContents = await dataParse.import(dataJsonParser).export(dataXlsxParser)
return fileContents.map(({ tableName, content }) => {
return {
name: name || `${tableName}.xlsx`,
@@ -96,18 +96,16 @@ export const generateExportDatas = (datas, fileType, name) => {
* @param {*} fileType 文件类型
* @returns 表结构 json
*/
-export const handleImportStruct = (files, fileType) => {
+export const handleImportStruct = async (files, fileType) => {
const dataParse = new DataParse()
const structJsonParser = new StructJsonParser()
let tableStructs = []
if (fileType === DATA_FILE_TYPE.SQL) {
- tableStructs = dataParse
- .set(new StructSqlParser(files))
- .export(structJsonParser)
+ await dataParse.set(new StructSqlParser(files))
+ tableStructs = await dataParse.export(structJsonParser)
} else {
- tableStructs = dataParse
- .set(new StructXlsxParser(files))
- .export(structJsonParser)
+ await dataParse.set(new StructXlsxParser(files))
+ tableStructs = await dataParse.export(structJsonParser)
}
// 执行校验
checkTable(tableStructs)
@@ -121,7 +119,7 @@ export const handleImportStruct = (files, fileType) => {
* @param {*} columns 字段信息
* @returns 数据 json or sql
*/
-export const handleImportData = (files, fileType, columns) => {
+export const handleImportData = async (files, fileType, columns) => {
const dataParse = new DataParse()
const dataJsonParser = new DataJsonParser()
if (fileType === DATA_FILE_TYPE.SQL) {
@@ -136,7 +134,8 @@ export const handleImportData = (files, fileType, columns) => {
return files
} else {
const dataXlsxParser = new DataXlsxParser(files)
- return dataParse.set(dataXlsxParser).export(dataJsonParser)
+ await dataParse.set(dataXlsxParser)
+ return dataParse.export(dataJsonParser)
}
}
diff --git a/lib/shared/excel/index.js b/lib/shared/excel/index.js
new file mode 100644
index 000000000..91886b87f
--- /dev/null
+++ b/lib/shared/excel/index.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 ExcelJS from 'exceljs'
+
+const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+
+function binaryStringToUint8Array (content) {
+ const bytes = new Uint8Array(content.length)
+ for (let i = 0; i < content.length; i++) { + bytes[i] = content.charCodeAt(i) & 0xff + } + return bytes +} + +function normalizeLoadInput (content) { + if (typeof content === 'string') { + return binaryStringToUint8Array(content) + } + return content +} + +function getCellValue (cell, { raw = false } = {}) { + if (!cell) { + return '' + } + const { value } = cell + if (value === null || value === undefined) { + return '' + } + if (!raw) { + if (cell.text !== undefined && cell.text !== null && cell.text !== '') { + return cell.text + } + if (value instanceof Date) { + return value + } + if (typeof value === 'object') { + if (value.text !== undefined) { + return value.text + } + if (value.result !== undefined) { + return value.result + } + if (value.richText) { + return value.richText.map(item => item.text).join('')
+ }
+ }
+ return value
+ }
+ if (typeof value === 'object' && value.result !== undefined) {
+ return value.result
+ }
+ return value
+}
+
+/**
+ * 解析 xlsx 第一个 sheet 为对象数组(首行作为表头)
+ * @param {string|ArrayBuffer|Uint8Array} content
+ * @param {{ raw?: boolean, cellDates?: boolean }} options
+ */
+export async function parseFirstSheetToJson (content, options = {}) {
+ const { raw = false } = options
+ const workbook = new ExcelJS.Workbook()
+ await workbook.xlsx.load(normalizeLoadInput(content))
+ const worksheet = workbook.worksheets[0]
+ if (!worksheet) {
+ return []
+ }
+
+ const headerMap = {}
+ const headerRow = worksheet.getRow(1)
+ headerRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {
+ const header = getCellValue(cell, { raw })
+ if (header !== '' && header !== undefined && header !== null) {
+ headerMap[colNumber] = header
+ }
+ })
+
+ const result = []
+ const rowCount = worksheet.rowCount
+ for (let rowNumber = 2; rowNumber <= rowCount; rowNumber++) { + const row = worksheet.getRow(rowNumber) + const record = {} + let hasValue = false + row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
+ const key = headerMap[colNumber]
+ if (key === undefined) {
+ return
+ }
+ const cellValue = getCellValue(cell, { raw })
+ if (cellValue !== '' && cellValue !== undefined && cellValue !== null) {
+ hasValue = true
+ }
+ record[key] = cellValue
+ })
+ if (hasValue) {
+ result.push(record)
+ }
+ }
+ return result
+}
+
+/**
+ * 由二维数组生成 xlsx Buffer
+ * @param {string} sheetName
+ * @param {Array>} rows
+ */
+export async function buildXlsxBufferFromAoa (sheetName, rows) {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet(sheetName || 'Sheet1')
+ worksheet.addRows(rows)
+ return workbook.xlsx.writeBuffer()
+}
+
+/**
+ * 浏览器端下载 xlsx
+ * @param {string} fileName
+ * @param {string} sheetName
+ * @param {Array>} rows
+ */
+export async function downloadXlsxFromAoa (fileName, sheetName, rows) {
+ const buffer = await buildXlsxBufferFromAoa(sheetName, rows)
+ const blob = new Blob([buffer], { type: XLSX_MIME })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = fileName
+ link.click()
+ URL.revokeObjectURL(url)
+}
diff --git a/lib/shared/security/sql-protection.js b/lib/shared/security/sql-protection.js
new file mode 100644
index 000000000..6d6558da9
--- /dev/null
+++ b/lib/shared/security/sql-protection.js
@@ -0,0 +1,12 @@
+/**
+ * 安全的SQL标识符转义函数
+ * @param {string} identifier 标识符(表名、列名等)
+ * @returns {string} 转义后的标识符
+ */
+export const escapeIdentifier = (identifier) => {
+ // 只允许字母、数字、下划线
+ if (!/^[a-zA-Z0-9_-]+$/.test(identifier)) {
+ throw new Error('标识符格式错误:只允许字母、数字、下划线和连字符')
+ }
+ return `\`${identifier}\``
+}
diff --git a/lib/shared/security/xss-protection.js b/lib/shared/security/xss-protection.js
new file mode 100644
index 000000000..2a1990c79
--- /dev/null
+++ b/lib/shared/security/xss-protection.js
@@ -0,0 +1,41 @@
+/**
+ * XSS 防护工具函数
+ * 提供统一的XSS过滤和防护功能
+ **/
+/**
+ * HTML实体编码映射表
+ */
+const HTML_ENTITIES = {
+ '&': '&',
+ '<': '<', + '>': '>',
+ '"': '"',
+ "'": ''',
+ '/': '/'
+}
+
+/**
+ * 转义HTML特殊字符,防止XSS攻击
+ * @param {string} str 需要转义的字符串
+ * @returns {string} 转义后的安全字符串
+ */
+export function escapeHtml (str) {
+ if (typeof str !== 'string') {
+ return str
+ }
+ return str.replace(/[&"'/]/g, (match) => HTML_ENTITIES[match])
+}
+
+/**
+ * 安全的JSON解析
+ * @param {string} jsonStr JSON字符串
+ * @param {*} defaultValue 默认值
+ * @returns {*} 解析后的对象或默认值
+ */
+export function safeJsonParse (jsonStr, defaultValue = null) {
+ try {
+ return JSON.parse(jsonStr)
+ } catch (e) {
+ return defaultValue
+ }
+}
diff --git a/package.json b/package.json
index 8d5677336..2072a6091 100644
--- a/package.json
+++ b/package.json
@@ -104,7 +104,7 @@
"app-root-path": "~3.0.0",
"async-validator": "~1.8.1",
"bk-lesscode-render": "1.0.0-beta.4",
- "axios": "~1.12.0",
+ "axios": "^1.15.2",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-parameter-decorator": "^1.0.16",
"better-npm-run": "~0.1.1",
@@ -115,7 +115,8 @@
"change-case": "~4.1.1",
"cheerio": "1.0.0-rc.2",
"co-views": "~2.1.0",
- "compressing": "^1.6.3",
+ "ejs": "^3.1.10",
+ "compressing": "^1.10.5",
"continuation-local-storage": "~3.2.1",
"cookie": "~0.4.0",
"core-js": "^3.25.5",
@@ -147,7 +148,7 @@
"js-base64": "^3.7.7",
"js-cookie": "3.0.1",
"jsdom": "~16.7.0",
- "koa": "~2.8.2",
+ "koa": "^2.16.4",
"koa-body": "^4.2.0",
"koa-bodyparser": "~4.2.1",
"koa-convert": "~1.2.0",
@@ -162,7 +163,7 @@
"koa-static": "~5.0.0",
"koa-unless": "^1.0.7",
"koa2-connect-history-api-fallback": "~0.1.2",
- "lodash": "~4.17.15",
+ "lodash": "^4.18.0",
"lodash.clonedeep": "~4.5.0",
"log4js-json-layout": "^2.2.2",
"lucky-canvas": "^1.7.27",
@@ -177,15 +178,15 @@
"mavon-editor": "^2.9.0",
"md5": "^2.3.0",
"monaco-editor": "^0.52.2",
- "monaco-editor-webpack-plugin":"^7.1.0",
- "moment": "^2.29.1",
+ "monaco-editor-webpack-plugin": "^7.1.0",
+ "moment": "^2.30.1",
"mysql2": "~2.3.3",
"node-request-context": "~1.0.5",
"node-sql-parser": "^4.5.1",
"nodemon": "~1.19.3",
"node-fetch": "^2.6.13",
"npm": "6.14.15",
- "path-to-regexp": "^8.3.0",
+ "path-to-regexp": "^8.4.2",
"pinyin": "^2.11.2",
"postcss": "~8.4.31",
"postcss-import": "^15.0.0",
@@ -200,11 +201,10 @@
"raw-loader": "^4.0.2",
"reflect-metadata": "^0.1.13",
"screenfull": "~5.0.2",
- "shelljs": "~0.8.4",
+ "shelljs": "^0.10.0",
"shx": "~0.3.2",
"swagger-ui-dist": "^5.11.7",
- "swig": "~1.4.2",
- "swiper": "11.0.5",
+ "swiper": "^12.1.2",
"swiper-element-animation": "^1.1.0",
"transliteration": "~2.1.8",
"tslib": "^2.0.3",
@@ -226,7 +226,7 @@
"vue-template-compiler": "~2.7.14",
"vuedraggable": "~2.23.2",
"vuex": "~3.1.1",
- "xlsx": "^0.18.5"
+ "exceljs": "^4.4.0"
},
"overrides": {
"@babel/runtime": "7.23.9",