Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 97 additions & 19 deletions packages/@core/ui-kit/form-ui/src/form-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,40 +511,118 @@

fieldMappingTime.forEach(
([field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD']) => {
// 处理多级路径的辅助函数
const setNestedValue = (
obj: Record<string, any>,
path: string,
value: any,
): void => {
const keys = path.split('.');
if (keys.length === 1) {
// 单级路径直接赋值
obj[path] = value;
} else {
// 多级路径创建嵌套对象
let target: Record<string, any> = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (key !== undefined) {
// 添加类型检查
if (
!(key in target) ||
typeof target[key] !== 'object' ||
target[key] === null
) {
target[key] = {};
}
target = target[key] as Record<string, any>;
}
}
const lastKey = keys[keys.length - 1];
if (lastKey !== undefined) {
target[lastKey] = value;

Check warning

Code scanning / CodeQL

Prototype-polluting function Medium

The property chain
here
is recursively assigned to
target
without guarding against prototype pollution.
}
}
};

Comment on lines +515 to +547
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Harden nested path helpers against prototype pollution and invalid segments

Guard against "proto", "prototype", "constructor" and empty segments. This also addresses the CodeQL warning.

Apply this diff:

-        const setNestedValue = (
+        const setNestedValue = (
           obj: Record<string, any>,
           path: string,
           value: any,
         ): void => {
-          const keys = path.split('.');
+          const keys = path.split('.').filter(Boolean);
+          // reject unsafe or empty paths
+          if (
+            keys.length === 0 ||
+            keys.some((k) => k === '__proto__' || k === 'prototype' || k === 'constructor')
+          ) {
+            console.warn('[form-api] Ignoring unsafe path:', path);
+            return;
+          }
           if (keys.length === 1) {
             // 单级路径直接赋值
             obj[path] = value;
           } else {
             // 多级路径创建嵌套对象
             let target: Record<string, any> = obj;
             for (let i = 0; i < keys.length - 1; i++) {
               const key = keys[i];
               if (key !== undefined) {
                 // 添加类型检查
-                if (
-                  !(key in target) ||
-                  typeof target[key] !== 'object' ||
-                  target[key] === null
-                ) {
+                if (
+                  !(key in target) ||
+                  typeof target[key] !== 'object' ||
+                  target[key] === null ||
+                  Array.isArray(target[key])
+                ) {
                   target[key] = {};
                 }
                 target = target[key] as Record<string, any>;
               }
             }
             const lastKey = keys[keys.length - 1];
             if (lastKey !== undefined) {
               target[lastKey] = value;
             }
           }
         };
 
         // 处理多级路径的删除函数
-        const deleteNestedProperty = (
+        const deleteNestedProperty = (
           obj: Record<string, any>,
           path: string,
         ): void => {
-          const keys = path.split('.');
+          const keys = path.split('.').filter(Boolean);
+          // reject unsafe or empty paths
+          if (
+            keys.length === 0 ||
+            keys.some((k) => k === '__proto__' || k === 'prototype' || k === 'constructor')
+          ) {
+            console.warn('[form-api] Ignoring unsafe path:', path);
+            return;
+          }
           if (keys.length === 1) {
             // 单级路径直接删除
             Reflect.deleteProperty(obj, path);
           } else {
             // 多级路径导航到目标对象并删除属性
             let target: Record<string, any> = obj;
             for (let i = 0; i < keys.length - 1; i++) {
               const key = keys[i];
               if (key !== undefined) {
                 // 添加类型检查
                 if (
                   !(key in target) ||
                   typeof target[key] !== 'object' ||
                   target[key] === null
                 ) {
                   return;
                 }
                 target = target[key] as Record<string, any>;
               }
             }
             const lastKey = keys[keys.length - 1];
             if (lastKey !== undefined) {
               Reflect.deleteProperty(target, lastKey);
             }
           }
         };

Also applies to: 549-579

🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 543-543: Prototype-polluting function
The property chain here is recursively assigned to target without guarding against prototype pollution.

// 处理多级路径的删除函数
const deleteNestedProperty = (
obj: Record<string, any>,
path: string,
): void => {
const keys = path.split('.');
if (keys.length === 1) {
// 单级路径直接删除
Reflect.deleteProperty(obj, path);
} else {
// 多级路径导航到目标对象并删除属性
let target: Record<string, any> = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (key !== undefined) {
// 添加类型检查
if (
!(key in target) ||
typeof target[key] !== 'object' ||
target[key] === null
) {
return;
}
target = target[key] as Record<string, any>;
}
}
const lastKey = keys[keys.length - 1];
if (lastKey !== undefined) {
Reflect.deleteProperty(target, lastKey);
}
}
};

// 类型安全检查
if (!field || !startTimeKey || !endTimeKey) {
return;
}

if (startTimeKey && endTimeKey && values[field] === null) {
Reflect.deleteProperty(values, startTimeKey);
Reflect.deleteProperty(values, endTimeKey);
// delete values[startTimeKey];
// delete values[endTimeKey];
deleteNestedProperty(values, startTimeKey);
deleteNestedProperty(values, endTimeKey);
}

if (!values[field]) {
Reflect.deleteProperty(values, field);
// delete values[field];
deleteNestedProperty(values, field);
return;
}

const timeRange = values[field];
if (!Array.isArray(timeRange) || timeRange.length < 2) {
return;
}

const [startTime, endTime] = values[field];
const [startTime, endTime] = timeRange;
if (format === null) {
values[startTimeKey] = startTime;
values[endTimeKey] = endTime;
setNestedValue(values, startTimeKey, startTime);
setNestedValue(values, endTimeKey, endTime);
} else if (isFunction(format)) {
values[startTimeKey] = format(startTime, startTimeKey);
values[endTimeKey] = format(endTime, endTimeKey);
setNestedValue(values, startTimeKey, format(startTime, startTimeKey));
setNestedValue(values, endTimeKey, format(endTime, endTimeKey));
} else {
const [startTimeFormat, endTimeFormat] = Array.isArray(format)
? format
: [format, format];

values[startTimeKey] = startTime
? formatDate(startTime, startTimeFormat)
: undefined;
values[endTimeKey] = endTime
? formatDate(endTime, endTimeFormat)
: undefined;
setNestedValue(
values,
startTimeKey,
startTime ? formatDate(startTime, startTimeFormat) : undefined,
);
setNestedValue(
values,
endTimeKey,
endTime ? formatDate(endTime, endTimeFormat) : undefined,
);
}
// delete values[field];
Reflect.deleteProperty(values, field);

deleteNestedProperty(values, field);
},
);
return values;
Expand Down
13 changes: 12 additions & 1 deletion playground/src/views/examples/form/basic.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const [BaseForm, baseFormApi] = useVbenForm({
class: 'w-full',
},
},
fieldMappingTime: [['rangePicker', ['startTime', 'endTime'], 'YYYY-MM-DD']],
fieldMappingTime: [['rangePicker', ['startTime', 'endTime'], 'YYYY-MM-DD'], ['rangePicker2', ['createTimeQuery.startTime', 'createTimeQuery.endTime'], 'YYYY-MM-DD HH:mm:ss']],
// 提交函数
handleSubmit: onSubmit,
handleValuesChange(_values, fieldsChanged) {
Expand Down Expand Up @@ -281,6 +281,17 @@ const [BaseForm, baseFormApi] = useVbenForm({
fieldName: 'rangePicker',
label: '范围选择器',
},
{
component: 'RangePicker',
fieldName: 'rangePicker2',
componentProps: {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
placeholder: ['开始日期', '结束日期'],
},
label: '时间范围选择器',
},
{
component: 'TimePicker',
fieldName: 'timePicker',
Expand Down
Loading