feat(waf): 新增安全防护节点 security_check

基础特征检测九项可开关;默认开启路径穿越与文件包含;命中任意规则走 false。
This commit is contained in:
ryan
2026-07-19 12:33:13 +08:00
parent db89f68547
commit b75f985815
18 changed files with 486 additions and 11 deletions
@@ -79,6 +79,7 @@ export function isConnectionAllowed(
ip_match: ['true', 'false'],
geo_match: ['true', 'false'],
ua_check: ['true', 'false'],
security_check: ['true', 'false'],
pow: ['next'],
};
return (
@@ -31,6 +31,7 @@ const handles: Partial<Record<WAFRuleNode['type'], string[]>> = {
ip_match: ['true', 'false'],
geo_match: ['true', 'false'],
ua_check: ['true', 'false'],
security_check: ['true', 'false'],
pow: ['next'],
};
@@ -49,6 +49,7 @@ describe('parseAddableNodeType', () => {
it('accepts addable types and rejects others', () => {
expect(parseAddableNodeType('ip_match')).toBe('ip_match');
expect(parseAddableNodeType('ua_check')).toBe('ua_check');
expect(parseAddableNodeType('security_check')).toBe('security_check');
expect(parseAddableNodeType('start')).toBeNull();
expect(parseAddableNodeType('')).toBeNull();
});
@@ -4,7 +4,7 @@ export const WAF_NODE_DRAG_MIME = 'application/openflare-waf-node';
export type AddableNodeType = Extract<
WAFRuleNode['type'],
'ip_match' | 'geo_match' | 'ua_check' | 'pow' | 'block'
'ip_match' | 'geo_match' | 'ua_check' | 'security_check' | 'pow' | 'block'
>;
export const NODE_TYPE_LABELS: Record<WAFRuleNode['type'], string> = {
@@ -12,6 +12,7 @@ export const NODE_TYPE_LABELS: Record<WAFRuleNode['type'], string> = {
ip_match: 'IP 匹配',
geo_match: '地域匹配',
ua_check: 'UA 检查',
security_check: '安全防护',
pow: 'PoW 挑战',
allow: '通过',
block: '阻止',
@@ -54,6 +55,23 @@ export function createRuleNode(
custom_ua_patterns: [],
},
};
if (type === 'security_check')
return {
id,
type,
position,
config: {
sql_injection: false,
path_traversal: true,
command_injection: false,
xss: false,
ssrf: false,
file_inclusion: true,
malicious_upload: false,
xxe: false,
crlf_injection: false,
},
};
if (type === 'pow')
return {
id,
@@ -79,6 +97,7 @@ export function parseAddableNodeType(value: string): AddableNodeType | null {
value === 'ip_match' ||
value === 'geo_match' ||
value === 'ua_check' ||
value === 'security_check' ||
value === 'pow' ||
value === 'block'
)
@@ -3,6 +3,7 @@ import {
Fingerprint,
Globe2,
ScanSearch,
Shield,
ShieldCheck,
} from 'lucide-react';
@@ -18,6 +19,7 @@ const items = [
{ type: 'ip_match' as const, icon: Fingerprint },
{ type: 'geo_match' as const, icon: Globe2 },
{ type: 'ua_check' as const, icon: ScanSearch },
{ type: 'security_check' as const, icon: Shield },
{ type: 'pow' as const, icon: ShieldCheck },
{ type: 'block' as const, icon: Ban },
] satisfies { type: AddableNodeType; icon: typeof Fingerprint }[];
@@ -25,7 +25,9 @@ it('hides match and block until UA check is enabled', () => {
const { rerender } = render(
<NodeProperties node={node} ipGroups={[]} onChange={onChange} />,
);
expect(screen.queryByRole('switch', { name: /屏蔽常见爬虫/ })).not.toBeInTheDocument();
expect(
screen.queryByRole('switch', { name: /屏蔽常见爬虫/ }),
).not.toBeInTheDocument();
expect(screen.queryByLabelText('浏览器')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('switch', { name: /开启 UA 检查/ }));
expect(onChange).toHaveBeenCalledWith(
@@ -40,9 +42,15 @@ it('hides match and block until UA check is enabled', () => {
onChange={onChange}
/>,
);
expect(screen.getByRole('switch', { name: /屏蔽常见爬虫/ })).toBeInTheDocument();
expect(screen.getByRole('switch', { name: /屏蔽非正常/ })).toBeInTheDocument();
expect(screen.getByRole('switch', { name: /屏蔽自定义/ })).toBeInTheDocument();
expect(
screen.getByRole('switch', { name: /屏蔽常见爬虫/ }),
).toBeInTheDocument();
expect(
screen.getByRole('switch', { name: /屏蔽非正常/ }),
).toBeInTheDocument();
expect(
screen.getByRole('switch', { name: /屏蔽自定义/ }),
).toBeInTheDocument();
expect(screen.getAllByLabelText('说明').length).toBeGreaterThan(0);
});
@@ -344,6 +344,95 @@ function PropertyFields({
)}
</FieldGroup>
);
if (node.type === 'security_check')
return (
<FieldGroup>
<DisplayNameField node={node} onChange={onChange} />
<div className='space-y-1'>
<p className='flex items-center gap-1.5 text-xs font-medium text-muted-foreground'>
安全防护
<FieldHelp tip='命中任意已启用规则返回 False' />
</p>
</div>
<Separator />
<div className='space-y-3'>
<p className='text-xs font-medium text-muted-foreground'>基础防护</p>
{(
[
{
key: 'path_traversal',
label: '路径穿越防护',
tip: '检测 Path / Query / Body 中的 ../ 与编码变种',
},
{
key: 'file_inclusion',
label: '文件包含(LFI/RFI)',
tip: '检测 Path / Query / Body 中的 php://、file://、/etc/passwd 等',
},
{
key: 'sql_injection',
label: 'SQL 注入',
tip: '检测 Query / Body / Header / Cookie 中的 SQL 注入特征',
},
{
key: 'command_injection',
label: '命令注入',
tip: '检测 Query / Body / Header 中的 OS 命令注入特征',
},
{
key: 'xss',
label: 'XSS',
tip: '检测 Query / Body / Header 中的反射型 XSS 特征',
},
{
key: 'ssrf',
label: 'SSRF',
tip: '检测 Query / Body 中的内网地址与危险协议',
},
{
key: 'malicious_upload',
label: '恶意文件上传',
tip: '检测 Multipart 文件名与危险扩展名',
},
{
key: 'xxe',
label: 'XXE',
tip: '检测 XML Body 中的外部实体特征',
},
{
key: 'crlf_injection',
label: 'CRLF 注入',
tip: '检测 Header / Query / Body 中的换行注入',
},
] as const
).map((item) => (
<Field
key={item.key}
orientation='horizontal'
className='items-center justify-between gap-3'
>
<FieldLabel
htmlFor={`${node.id}-${item.key}`}
className='flex items-center gap-1.5'
>
{item.label}
<FieldHelp tip={item.tip} />
</FieldLabel>
<Switch
id={`${node.id}-${item.key}`}
checked={node.config[item.key]}
onCheckedChange={(checked) =>
onChange({
...node,
config: { ...node.config, [item.key]: checked },
})
}
/>
</Field>
))}
</div>
</FieldGroup>
);
if (node.type === 'pow')
return (
<FieldGroup>
@@ -6,6 +6,7 @@ import {
Globe2,
Play,
ScanSearch,
Shield,
ShieldCheck,
} from 'lucide-react';
@@ -25,6 +26,7 @@ const meta = {
ip_match: { icon: Fingerprint },
geo_match: { icon: Globe2 },
ua_check: { icon: ScanSearch },
security_check: { icon: Shield },
pow: { icon: ShieldCheck },
allow: { icon: Flag },
block: { icon: Ban },
@@ -35,6 +37,7 @@ const outputHandles: Partial<Record<WAFRuleNode['type'], string[]>> = {
ip_match: ['true', 'false'],
geo_match: ['true', 'false'],
ua_check: ['true', 'false'],
security_check: ['true', 'false'],
pow: ['next'],
};
+19
View File
@@ -790,6 +790,18 @@ export interface UACheckConfig {
custom_ua_patterns: string[];
}
export interface SecurityCheckConfig {
sql_injection: boolean;
path_traversal: boolean;
command_injection: boolean;
xss: boolean;
ssrf: boolean;
file_inclusion: boolean;
malicious_upload: boolean;
xxe: boolean;
crlf_injection: boolean;
}
export type WAFRuleNode =
| {
id: string;
@@ -819,6 +831,13 @@ export type WAFRuleNode =
position: XYPosition;
config: UACheckConfig;
}
| {
id: string;
type: 'security_check';
label?: string;
position: XYPosition;
config: SecurityCheckConfig;
}
| {
id: string;
type: 'pow';