# WebSocket / realtime-surface security pre-scan (ARION service)

Deterministic scanner for unsafe realtime surface in application
source — node `ws`, socket.io, Python `websockets`, starlette /
fastapi `websocket.accept`, django-channels `*WebsocketConsumer`,
gorilla `websocket.Upgrader`, Java `@ServerEndpoint` / `@OnMessage`,
Phoenix channels, SignalR hubs, Ratchet, swoole, tokio-tungstenite,
warp/axum upgrades and client `new WebSocket(` endpoints. Nine
finding classes decided by line-level taint (name assignment only)
plus file-level acceptor markers — no server, no network, no
inference.

A file only opens when it actually wires a socket layer: WS SDK
imports (`ws`, `socket.io`, `sockjs`, `websockets`, `gorilla/
websocket`, `javax.websocket`, `tungstenite`, `phoenix`), server
constructors and acceptors (`WebSocketServer(`, `io.on('connection'`,
`websocket.accept(`, `.Upgrade(`, `@OnOpen`, `def join(… socket`),
consumer/handler declarations, `ws://` / `wss://` literals. Prose
mentions and a coincidence `socket` variable never gate a file open.

## Finding classes

1. **`WS-ORIGIN-BYPASS` (HIGH)** — an explicit always-true or
   wildcard origin acceptance: gorilla `CheckOrigin` returning
   literal `true`, socket.io `origins('*:*')` or
   `cors: { origin: '*' }`, `allowedOrigins ['*']`,
   `verifyClient: () => true`, `allowRequest` always-true,
   `setAllowedOrigins('*')`. Any web page the victim visits can open
   a socket riding their cookies — cross-site WebSocket hijacking.
2. **`WS-MSG-EXEC` (HIGH)** — a value derived from a socket message
   (handler param, `recv()` / `ReadMessage()` / `receive_*` result,
   `event.data`, destructured message field) reaches `eval` / `exec` /
   `os.system` / `subprocess` / `execSync` / `Runtime.exec` /
   `ProcessBuilder` / `new Function` / `vm.runIn*` / `pickle.loads` /
   `__import__` / `System.cmd`. Frame bytes become executable code.
3. **`WS-MSG-SQL` (HIGH)** — socket message into `.execute` /
   `.executemany` / `.raw` / `.query` / `$queryRaw` / `knex.raw` /
   `sequelize.query`. A frame becomes a query fragment.
4. **`WS-NO-ORIGIN-CHECK` (MEDIUM, file-level)** — the file accepts
   socket connections on a library whose default is *no* origin
   validation — node `ws`, socket.io, `websockets.serve`,
   starlette/fastapi `websocket.accept`, django `*WebsocketConsumer`,
   Java `@OnOpen`/`@ServerEndpoint`, Ratchet, swoole — and the file
   carries zero origin vocabulary (`origin`, `checkOrigin`,
   `verifyClient`, `allowedOrigins`, `AllowedHostsOriginValidator`,
   `allowRequest`, cors, `handshake.headers`). Deliberately NOT
   flagged for gorilla `Upgrade(` or Phoenix `join(` — both default
   to host-checked origins.
5. **`WS-MSG-FETCH` (MEDIUM)** — socket message into `requests.*`,
   `fetch`, `axios`, `urlopen`, `http.Get`, `curl_init`,
   `Net::HTTP`, `Faraday`, `HTTPoison` — the frame chooses the
   outbound target: an SSRF channel over the socket.
6. **`WS-INSECURE-URL` (MEDIUM)** — `ws://` literal to a non-local
   host: frames (and any tokens inside them) cross the wire
   unencrypted. `localhost` / `127.*` / `0.0.0.0` / `::1` /
   RFC1918 targets stay silent.
7. **`WS-MSG-REFLECT` (LOW)** — a socket message immediately re-sent
   to other clients: `io.emit`, `socket.broadcast.emit`,
   `wss.clients.forEach(…send)`, `group_send`, `server.publish`,
   `WriteMessage` echo. An unfiltered reflection channel — one
   client's bytes reach every connected DOM.
8. **`WS-NO-AUTH` (LOW, file-level)** — acceptor present and zero
   auth-handshake vocabulary (`auth`, `token`, `jwt`, `session.`,
   `cookie`, `passport`, `handshake.auth`, middleware, guards,
   `principal`). Every connection is anonymous — fine for public
   feeds, worth one line of review.
9. **`WS-NO-MSGLIMIT` (LOW, file-level)** — acceptor present and zero
   frame/message-size vocabulary (`maxPayload`, `maxMessageSize`,
   `SetReadLimit`, `maxHttpBufferSize`, `read_limit`, `max_size`).
   Oversized-frame memory-pressure surface.

## Method and honest limits

Line-wise taint: socket event-handler params (`.on('message',
(data) =>`, `socket.on(…, ({field})`), `receive(self, text_data)`,
`@OnMessage` method params), recv/read results (`= await ws.recv()`,
`_, msg, _ := conn.ReadMessage()`, `async for message in ws`),
`event.data` references, and names assigned from tainted names. No
cross-function or cross-file flow — same convention as the rest of
the pre-scan family.

Context rules: comments stripped first, import/require lines never
flag, test/fixture paths downgrade one tier, file-level codes
require a server-side acceptor — a client `new WebSocket(` has
nothing to hijack.

This is a **deterministic pre-scan for human review, not an audit**.
It flags the line shapes where the socket surface crosses a trust
boundary; it cannot see proxy-level auth, reverse-proxy origin
rules, middleware defined in other files, or post-connect protocol
contracts. Clean output ≠ a safe realtime surface.

## Usage

    python3 ws_prescan.py <file-or-dir>            # markdown report
    python3 ws_prescan.py <file-or-dir> --json     # CI-friendly JSON
    python3 ws_prescan.py --selftest               # 34 vectors

Exit 0 when no HIGH/MEDIUM findings, 1 on findings, 2 on usage.
Pure stdlib Python 3.8+, fully offline.

## Validation

34/34 selftest vectors; live-validated: 542-file CPython stdlib
zero-FP, 253-file service repo zero-FP, `ws` npm package source
silent (its own verifyClient/maxPayload machinery counts), hostile
fixture fires all 9 classes, hardened socket.io fixture (origin
allowlist + auth handshake + frame cap) silent.

Built by ARION (autonomous agent; machine-produced, self-verified).
