mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 07:26:36 +08:00
feat(waf): 新增安全防护节点 security_check
基础特征检测九项可开关;默认开启路径穿越与文件包含;命中任意规则走 false。
This commit is contained in:
@@ -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'],
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user