Skip to content

Commit 74e2c47

Browse files
committed
refactor(core): decouple security validation via injectable policy
- Introduced SecurityPolicy interface to manage object and config sanitization. - Removed hardcoded web-specific logic (CSS, LayoutBox) from core validation. - Enabled dependency injection for platform-specific security strategies. - Achieved true headless architecture for the profile validation layer.
1 parent 941a5b4 commit 74e2c47

11 files changed

Lines changed: 73 additions & 243 deletions

File tree

packages/core/src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { ACTION_TYPES, BUTTON_MAP, CMP_TYPES, CONTEXT, STANDARD_ANCHORS, STANDARD_KEYS, VALID_UNITS } from './constants';
1+
import {
2+
ACTION_TYPES,
3+
BUTTON_MAP,
4+
CMP_TYPES,
5+
CONTEXT,
6+
STANDARD_ANCHORS,
7+
STANDARD_KEYS,
8+
VALID_UNITS,
9+
} from './constants';
210

311
export * from './runtime';
412
export * from './types';

packages/core/src/runtime/profile.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,37 @@ import {
99
import { Registry } from '../singletons/Registry';
1010
import { BaseEntity } from '../entities/BaseEntity';
1111
import { altDeepClone } from '../utils/object';
12-
import { sanitizeCssClass, sanitizePrototypePollution } from '../utils/security';
13-
import { compressLayoutBox, validateLayoutBox } from '../utils/layout';
12+
import { compressLayoutBox } from '../utils/layout';
13+
14+
/**
15+
* Interface defining the security and validation strategy for the engine.
16+
* Decouples core logic from platform-specific checks (like CSS validation).
17+
*/
18+
export interface SecurityPolicy {
19+
/**
20+
* Scans and sanitizes the raw object (e.g., preventing prototype pollution).
21+
*/
22+
sanitizeObject: <T>(raw: T) => T;
23+
24+
/**
25+
* Validates and normalizes the config object of an item.
26+
* This is where platform-specific checks (CSS units, class names) should live.
27+
*/
28+
validateConfig: (type: string, config: any) => any;
29+
}
30+
31+
/** Default "Pass-through" policy for non-secure or test environments */
32+
let _currentPolicy: SecurityPolicy = {
33+
sanitizeObject: (obj) => obj,
34+
validateConfig: (_, cfg) => cfg,
35+
};
36+
37+
/**
38+
* Injects a security policy into the core engine.
39+
*/
40+
export function setSecurityPolicy(policy: Partial<SecurityPolicy>) {
41+
_currentPolicy = { ..._currentPolicy, ...policy };
42+
}
1443

1544
const MAX_PROFILE_ITEMS = 100; // 单个配置允许的最大组件数
1645
const MAX_PROFILE_SIZE = 512 * 1024; // 512kB
@@ -40,7 +69,7 @@ export function validateProfile(raw: any): OmniPadProfile {
4069
}
4170

4271
// 过滤原型链污染
43-
raw = sanitizePrototypePollution(raw);
72+
raw = _currentPolicy.sanitizeObject(raw);
4473

4574
// ID 唯一性检查
4675
const idSet = new Set<string>();
@@ -68,12 +97,12 @@ export function validateProfile(raw: any): OmniPadProfile {
6897
id: String(item.id),
6998
type: String(item.type),
7099
parentId: item.parentId ? String(item.parentId) : undefined,
71-
// 确保 config 存在,业务参数平铺于此
72-
config: {
73-
...item.config,
74-
layout: validateLayoutBox(item.config.layout),
75-
cssClass: sanitizeCssClass(item.config.cssClass),
76-
},
100+
config: _currentPolicy.validateConfig(item.type, item.config || {}),
101+
// config: {
102+
// ...item.config,
103+
// layout: validateLayoutBox(item.config.layout),
104+
// cssClass: sanitizeCssClass(item.config.cssClass),
105+
// },
77106
};
78107
});
79108

packages/core/src/types/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export type FlexibleLength = ParsedLength | string | number;
8686
/**
8787
* Anchor position used to determine the alignment of an element relative to its coordinates.
8888
*/
89-
export type AnchorPoint = typeof STANDARD_ANCHORS[number];
89+
export type AnchorPoint = (typeof STANDARD_ANCHORS)[number];
9090

9191
/**
9292
* =================================================================

packages/core/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './cache';
22
export * from './distill';
33
export * from './id';
4+
export * from './layout';
45
export * from './math';
56
export * from './object';

packages/core/src/utils/layout.ts

Lines changed: 1 addition & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,4 @@
1-
import { VALID_UNITS } from '../constants';
2-
import { LayoutBox, CssUnit, ParsedLength, FlexibleLength } from '../types';
3-
import { sanitizeDomString } from './security';
4-
5-
/**
6-
* Convert the length input into a sanitized ParsedLength
7-
*
8-
* @param input - The raw length input.
9-
* @returns A sanitized ParsedLength.
10-
*/
11-
export function parseLength(input: FlexibleLength | undefined): ParsedLength | undefined {
12-
// 1. 处理空值或无效值
13-
if (input == null) {
14-
return undefined;
15-
}
16-
17-
// 2. 处理 ParsedLength 对象
18-
if (typeof input === 'object' && 'unit' in input && 'value' in input) {
19-
return sanitizeParsedLength(input);
20-
}
21-
22-
// 3. 处理纯数字:默认 px
23-
if (typeof input === 'number') {
24-
return {
25-
value: Number.isFinite(input) ? input : 0,
26-
unit: 'px',
27-
};
28-
}
29-
30-
// 4. 处理字符串
31-
const val = input.trim().toLowerCase();
32-
const numericPart = parseFloat(val);
33-
34-
// 检查数字部分是否有效
35-
if (isNaN(numericPart)) {
36-
return { value: 0, unit: 'px' };
37-
}
38-
39-
// 直接截取剩下的所有内容作为单位
40-
const unitPart = val.slice(String(numericPart).length).trim();
41-
42-
return sanitizeParsedLength({ value: numericPart, unit: unitPart as CssUnit });
43-
}
44-
45-
/**
46-
* Check the whitelist of verification units and sanitize ParsedLength.
47-
*/
48-
export const sanitizeParsedLength = (parsed: ParsedLength): ParsedLength => {
49-
const { value, unit } = parsed;
50-
51-
if (!isNaN(value) && (VALID_UNITS as readonly string[]).includes(unit)) {
52-
return { value, unit };
53-
}
54-
55-
// 非法单位,降级为 px
56-
console.warn(`[OmniPad-Core] Blocked invalid CSS unit: ${unit}`);
57-
return { value: isNaN(value) ? 0 : value, unit: 'px' };
58-
};
1+
import { LayoutBox, ParsedLength } from '../types';
592

603
/**
614
* Convert the ParsedLength back to a CSS string
@@ -64,23 +7,6 @@ export const lengthToCss = (parsed: ParsedLength | undefined): string | undefine
647
return parsed == null ? undefined : `${parsed.value}${parsed.unit}`;
658
};
669

67-
/**
68-
* Validate a raw LayoutBox config.
69-
*/
70-
export function validateLayoutBox(raw: LayoutBox): LayoutBox {
71-
return {
72-
...raw,
73-
left: parseLength(raw.left),
74-
top: parseLength(raw.top),
75-
right: parseLength(raw.right),
76-
bottom: parseLength(raw.bottom),
77-
width: parseLength(raw.width),
78-
height: parseLength(raw.height),
79-
// 关键:对选择器和类名进行脱毒处理 / Critical: Sanitize selector and class names
80-
stickySelector: sanitizeDomString(raw.stickySelector),
81-
};
82-
}
83-
8410
/**
8511
* Compress layout properties into css strings.
8612
*/

packages/core/src/utils/security.ts

Lines changed: 0 additions & 127 deletions
This file was deleted.

packages/vue/src/components/VirtualButton.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ const defaultProps = {
3535
};
3636
3737
const { uid, core, state, domEvents, effectiveConfig, effectiveLayout, elementRef } =
38-
useWidgetSetup<ButtonCore, ButtonState, ButtonConfig>(OmniPad.Types.BUTTON, props, { defaultProps });
38+
useWidgetSetup<ButtonCore, ButtonState, ButtonConfig>(OmniPad.Types.BUTTON, props, {
39+
defaultProps,
40+
});
3941
4042
// 转发交互
4143
const onPointerDown = (e: PointerEvent) => domEvents?.onPointerDown?.(e);

packages/vue/src/composables/useWidgetConfig.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ export function useWidgetConfig<T extends BaseConfig>(
3535
const treeNode = validateWidgetNode(props.treeNode, requiredType);
3636

3737
// 确定父节点编号
38-
const injectedParentId = inject<Ref<string | undefined>>(OmniPad.Context.PARENT_ID_KEY, ref(undefined));
38+
const injectedParentId = inject<Ref<string | undefined>>(
39+
OmniPad.Context.PARENT_ID_KEY,
40+
ref(undefined),
41+
);
3942
const parentId = computed(() => {
4043
return props.parentId || treeNode?.config?.parentId || injectedParentId.value;
4144
});

packages/web/src/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import {
44
setGamepadProvider,
55
setGlobalSignalHandler,
66
setRafProvider,
7+
setSecurityPolicy,
78
} from '@omnipad/core';
89
import { dispatchKeyboardEvent } from './dom/action';
10+
import { sanitizeCssClass, sanitizePrototypePollution } from './ts/security';
11+
import { validateLayoutBox } from './ts/layout';
912

1013
export * from './dom';
1114
export * from './ts';
@@ -38,3 +41,15 @@ if (typeof navigator !== 'undefined' && navigator.getGamepads) {
3841
return gamepadSnapshot;
3942
});
4043
}
44+
45+
setSecurityPolicy({
46+
sanitizeObject: (obj) => sanitizePrototypePollution(obj),
47+
48+
validateConfig: (_, config) => {
49+
return {
50+
...config,
51+
layout: validateLayoutBox(config.layout),
52+
cssClass: sanitizeCssClass(config.cssClass),
53+
};
54+
},
55+
});

0 commit comments

Comments
 (0)