Skip to content
123 changes: 106 additions & 17 deletions packages/@core/ui-kit/form-ui/src/form-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { isRef, toRaw } from 'vue';
import { Store } from '@vben-core/shared/store';
import {
bindMethods,
createMerge,
formatDate,
isDate,
isDayjsObject,
Expand Down Expand Up @@ -330,24 +329,114 @@ export class FormApi {
}

/**
* 合并算法有待改进,目前的算法不支持object类型的值。
* antd的日期时间相关组件的值类型为dayjs对象
* element-plus的日期时间相关组件的值类型可能为Date对象
* 以上两种类型需要排除深度合并
* 深度合并两个对象,支持嵌套对象、数组直接覆盖,忽略 Date 和 Dayjs 对象深度合并
*
* 主要用于合并表单当前值 [target] 与传入的新值 [source]
* 合并策略:
* - 基本类型直接覆盖
* - 数组直接替换
* - 非日期类对象进行递归合并
*
* @param target - 当前对象(通常是 form.values)
* @param source - 新传入的对象(通常是 fields)
* @returns 返回合并后的新对象,不修改原对象
*
* @example
* fieldMergeFn({ a: { b: 1 } }, { a: { c: 2 }, d: 3 });
* // 返回: { a: { b: 1, c: 2 }, d: 3 }
*/
const fieldMergeFn = createMerge((obj, key, value) => {
if (key in obj) {
obj[key] =
!Array.isArray(obj[key]) &&
isObject(obj[key]) &&
!isDayjsObject(obj[key]) &&
!isDate(obj[key])
? fieldMergeFn(obj[key], value)
: value;
const fieldMergeFn = (
target: Record<string, any>,
source: Record<string, any>,
) => {
const result = { ...target };

for (const key in source) {
const targetValue = result[key];
const sourceValue = source[key];

// 如果 sourceValue 是 null 或 undefined,保留旧值
if (sourceValue === null || sourceValue === undefined) {
continue;
}

// 如果 sourceValue 是数组,直接覆盖
if (Array.isArray(sourceValue)) {
result[key] = sourceValue;
}
// 如果 sourceValue 是对象(非 Date、非 Dayjs),进行深度合并
else if (
isObject(sourceValue) &&
!isDate(sourceValue) &&
!isDayjsObject(sourceValue)
) {
result[key] =
isObject(targetValue) &&
!isDate(targetValue) &&
!isDayjsObject(targetValue)
? fieldMergeFn(targetValue, sourceValue)
: sourceValue;
}
// 其他情况(如基本类型)直接赋值
else {
result[key] = sourceValue;
}
}
return true;
});
const filteredFields = fieldMergeFn(fields, form.values);

return result;
};
Comment on lines +348 to +387
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Harden deep-merge: prevent prototype pollution, avoid for-in over prototypes, and clone arrays to avoid aliasing

  • Security: Merging untrusted input can allow writing to proto/prototype/constructor keys, leading to prototype pollution.
  • Correctness: for..in walks inherited enumerable props. Prefer Object.keys.
  • Immutability: Assigning arrays by reference means subsequent mutations can leak back to the input object.

Apply these changes:

-    const fieldMergeFn = (
+    const fieldMergeFn = (
       target: Record<string, any>,
       source: Record<string, any>,
     ) => {
       const result = { ...target };
 
-      for (const key in source) {
-        const targetValue = result[key];
-        const sourceValue = source[key];
+      for (const key of Object.keys(source)) {
+        // Guard against prototype pollution
+        if (key === '__proto__' || key === 'prototype' || key === 'constructor') {
+          continue;
+        }
+        const targetValue = result[key];
+        const sourceValue = (source as Record<string, any>)[key];
 
         // 如果 sourceValue 是 null 或 undefined,保留旧值
-        if (sourceValue === null || sourceValue === undefined) {
+        if (sourceValue == null) {
           continue;
         }
 
         // 如果 sourceValue 是数组,直接覆盖
-        if (Array.isArray(sourceValue)) {
-          result[key] = sourceValue;
+        if (Array.isArray(sourceValue)) {
+          // shallow-clone array to avoid sharing references
+          result[key] = sourceValue.slice();
         }
         // 如果 sourceValue 是对象(非 Date、非 Dayjs),进行深度合并
         else if (
           isObject(sourceValue) &&
           !isDate(sourceValue) &&
           !isDayjsObject(sourceValue)
         ) {
-          result[key] =
-            isObject(targetValue) &&
-            !isDate(targetValue) &&
-            !isDayjsObject(targetValue)
-              ? fieldMergeFn(targetValue, sourceValue)
-              : sourceValue;
+          result[key] =
+            isObject(targetValue) &&
+            !isDate(targetValue) &&
+            !isDayjsObject(targetValue)
+              ? fieldMergeFn(targetValue, sourceValue)
+              // shallow-clone object to avoid sharing references with source
+              : { ...sourceValue };
         }
         // 其他情况(如基本类型)直接赋值
         else {
           result[key] = sourceValue;
         }
       }
 
       return result;
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fieldMergeFn = (
target: Record<string, any>,
source: Record<string, any>,
) => {
const result = { ...target };
for (const key in source) {
const targetValue = result[key];
const sourceValue = source[key];
// 如果 sourceValue 是 null 或 undefined,保留旧值
if (sourceValue === null || sourceValue === undefined) {
continue;
}
// 如果 sourceValue 是数组,直接覆盖
if (Array.isArray(sourceValue)) {
result[key] = sourceValue;
}
// 如果 sourceValue 是对象(非 Date、非 Dayjs),进行深度合并
else if (
isObject(sourceValue) &&
!isDate(sourceValue) &&
!isDayjsObject(sourceValue)
) {
result[key] =
isObject(targetValue) &&
!isDate(targetValue) &&
!isDayjsObject(targetValue)
? fieldMergeFn(targetValue, sourceValue)
: sourceValue;
}
// 其他情况(如基本类型)直接赋值
else {
result[key] = sourceValue;
}
}
return true;
});
const filteredFields = fieldMergeFn(fields, form.values);
return result;
};
const fieldMergeFn = (
target: Record<string, any>,
source: Record<string, any>,
) => {
const result = { ...target };
for (const key of Object.keys(source)) {
// Guard against prototype pollution
if (key === '__proto__' || key === 'prototype' || key === 'constructor') {
continue;
}
const targetValue = result[key];
const sourceValue = (source as Record<string, any>)[key];
// 如果 sourceValue 是 null 或 undefined,保留旧值
if (sourceValue == null) {
continue;
}
// 如果 sourceValue 是数组,直接覆盖
if (Array.isArray(sourceValue)) {
// shallow-clone array to avoid sharing references
result[key] = sourceValue.slice();
}
// 如果 sourceValue 是对象(非 Date、非 Dayjs),进行深度合并
else if (
isObject(sourceValue) &&
!isDate(sourceValue) &&
!isDayjsObject(sourceValue)
) {
result[key] =
isObject(targetValue) &&
!isDate(targetValue) &&
!isDayjsObject(targetValue)
? fieldMergeFn(targetValue, sourceValue)
// shallow-clone object to avoid sharing references with source
: { ...sourceValue };
}
// 其他情况(如基本类型)直接赋值
else {
result[key] = sourceValue;
}
}
return result;
};
🤖 Prompt for AI Agents
In packages/@core/ui-kit/form-ui/src/form-api.ts around lines 348 to 387, the
deep-merge allows prototype pollution, iterates inherited props with for..in,
and assigns arrays by reference; fix by iterating only own keys (use
Object.keys(source)), skip dangerous keys like "__proto__", "prototype", and
"constructor" (check and continue), clone arrays before assigning (e.g., create
a shallow copy), and keep the existing deep-merge recursion but only when both
source and target are plain objects; ensure you do not traverse inherited
properties and do not assign array references directly to avoid aliasing.


/**
* 从对象中提取指定字段路径的子集,支持多级嵌套字段(如 'user.address.city')
*
* @param obj - 要从中提取字段的对象
* @param filedNames - 字段路径数组,可以是多级字段(例如 ['user.name', 'user.age'])
* @returns 返回一个新对象,仅包含 `filedNames` 所指定的字段及其值
*
* @example
* const obj = {
* user: { name: 'Alice', age: 25 },
* email: '[email protected]'
* };
* pickFields(obj, ['user.name', 'email']);
* // 返回: { user: { name: 'Alice' }, email: '[email protected]' }
*/
function pickFields(obj: Record<string, any>, filedNames: string[]) {
const result: Record<string, any> = {};

for (const path of filedNames) {
const keys: string[] = path.split('.');
let value: any = obj;
let target: any = result;

for (let i = 0; i < keys.length; i++) {
const key = keys[i] as string;

if (value && typeof value === 'object' && key in value) {
value = value[key];
if (i === keys.length - 1) {
// 最后一级字段存在才赋值
target[key] = value;
} else {
// 初始化中间结构
target[key] = target[key] || {};
target = target[key];
}
} else {
// 路径不存在则跳过
break;
}
}
}

return result;
}
Comment on lines +389 to +433
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Harden pickFields: block prototype keys, use hasOwnProperty, and fix typo in parameter name

  • Security: Prevent paths containing proto/prototype/constructor.
  • Correctness: Use Object.prototype.hasOwnProperty to avoid inherited props.
  • Typo: filedNames → fieldNames. Also trim and skip empty paths.
-    function pickFields(obj: Record<string, any>, filedNames: string[]) {
+    function pickFields(obj: Record<string, any>, fieldNames: string[]) {
       const result: Record<string, any> = {};
 
-      for (const path of filedNames) {
-        const keys: string[] = path.split('.');
+      for (const rawPath of fieldNames) {
+        const path = String(rawPath).trim();
+        if (!path) continue;
+        const keys: string[] = path.split('.');
         let value: any = obj;
         let target: any = result;
 
         for (let i = 0; i < keys.length; i++) {
           const key = keys[i] as string;
 
-          if (value && typeof value === 'object' && key in value) {
+          // Block prototype-polluting keys
+          if (key === '__proto__' || key === 'prototype' || key === 'constructor') {
+            break;
+          }
+
+          if (
+            value &&
+            typeof value === 'object' &&
+            Object.prototype.hasOwnProperty.call(value, key)
+          ) {
             value = value[key];
             if (i === keys.length - 1) {
               // 最后一级字段存在才赋值
               target[key] = value;
             } else {
               // 初始化中间结构
-              target[key] = target[key] || {};
+              target[key] = isObject(target[key]) ? target[key] : {};
               target = target[key];
             }
           } else {
             // 路径不存在则跳过
             break;
           }
         }
       }
 
       return result;
     }

Committable suggestion skipped: line range outside the PR's diff.


const fieldNames = (this.state?.schema ?? []).map((item) => item.fieldName);
const filteredFields = pickFields(
fieldMergeFn(form.values, fields),
fieldNames,
);
this.handleStringToArrayFields(filteredFields);
form.setValues(filteredFields, shouldValidate);
}
Expand Down
Loading