WebSocket Security
SkillCommunicationSecuring the WebSocket upgrade and the frames after it: Origin validation against Cross-Site WebSocket Hijacking, authenticating the handshake rather than the first message, the ticket pattern for browsers that cannot set headers, per-frame authorization, and resource limits on a connection that stays open. Use when generating a WebSocket, Socket.IO, SignalR, or Phoenix Channels server, wiring real-time messaging, presence, or collaborative editing, or reviewing a /ws endpoint.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the WebSocket Security skill
What this skill tells your AI
The instructions your AI receives, as published by shieldnet-360/secure-vibe in skills/websocket-security/SKILL.md and read by ahel’s review.
Rules (for AI agents)
ALWAYS
- Validate the
Originheader on the upgrade handshake against an allowlist, and understand precisely what that buys: the browser setsOriginand script cannot forge it, so the check stops Cross-Site WebSocket Hijacking — a page onattacker.comopeningwss://api.example.com/wsand inheriting the user's cookies. It buys nothing against a non-browser client, which sets any header it likes. Origin is a CSRF control, not authentication. The same-origin policy and CORS preflight do not apply to the upgrade at all, which is why this check must be written by hand. - Authenticate the handshake, not the first frame. Once the upgrade completes the
connection exists, has consumed the user's cookie context, and is already a resource
the caller controls. A
subscribeorauthmessage afterwards is too late. - For a browser client, pick one of two mechanisms, because the browser
WebSocketconstructor cannot set request headers — there is noAuthorizationheader available to you:- Cookie on the upgrade plus the
Origincheck above as the CSRF control, or - A single-use ticket: the authenticated page calls an ordinary HTTP endpoint, receives a short-lived opaque ticket bound to the user and to the intended resource, passes it in the URL, and the server redeems and invalidates it at handshake time. A ticket is safe in a URL precisely because replaying it fails.
- Cookie on the upgrade plus the
- Re-authorize every frame, both the action and the subject it names. A frame like
{"action":"write","subjectId":"X"}must be checked against the principal established at handshake — an authenticated socket must never be able to assert an arbitrary subject id per frame. That is per-frame BOLA, and the forged id reaches every consumer downstream of the socket that trusts it. Permissions also change while the connection is open: logout, role change, account lock, token expiry. A socket opened an hour ago carries an authorization decision an hour old. - Bound the connection: maximum frame and message size, messages per second per
connection, concurrent connections per user and per source address, and total
connection lifetime after which the client must re-handshake. A WebSocket is an
allocation that persists, so every limit HTTP gets per-request must be re-expressed
per-connection. Concrete starting values, and the backpressure and heartbeat
settings, are in
references/limits-and-handshake.md. - Use
wss://.protocol-securityowns the TLS configuration behind it; what belongs here is thatws://exposes the session token, every frame, and the handshake itself to any on-path observer. - Consult
auth-securityfor the token's own properties — lifetime, rotation, and why a bearer token does not belong in a URL — andcors-securityfor the origin allowlist itself, including how to treat the serializednullorigin.
NEVER
- Skip Origin validation on the grounds that "CORS does not apply to WebSockets." That is the reason the check is required, not a reason to omit it.
- Put a bearer token — a JWT, a session id, an API key — in the WebSocket URL.
URLs reach access logs, proxy logs,
Refererheaders and browser history, and a token that survives being read there is a token an attacker can replay. The single-use ticket above is the form that is safe in a URL; a reusable credential is not, andauth-securityowns that rule for every other transport. - Treat a session cookie as a long-lived connection credential that outlives the session it came from. If the socket must survive re-authentication, redeem a fresh ticket on reconnect.
- Let a client-supplied subprotocol influence server-side routing or handler selection without an allowlist. Subprotocol negotiation is attacker-controlled input.
- Run WebSocket handlers on the same bounded thread pool or event loop as HTTP request handlers with no separate sizing. A slow or chatty socket then starves ordinary request work, which is the same failure as a slow-loris with a longer lease.
- Put internal topology into frames — pod names, node ids, upstream hostnames, internal error text. A real-time channel is a chatty reconnaissance surface.
- Reconnect without bounded exponential backoff and jitter on the client. An outage otherwise converts every client into a synchronized retry flood at the moment the service is least able to absorb it.
KNOWN FALSE POSITIVES
- A deliberately public endpoint — a public presence or broadcast feed carrying no per-user data — may accept any origin. What it may not skip is the per-connection and per-source limits, because an open endpoint is the one most exposed to them.
- A native mobile or desktop client sends no
Originheader at all, which is a different case from the stringnull. Absent means "not a browser": decide whether to admit it and authenticate it some other way.nullmeans an opaque origin — a sandboxed iframe, adata:URL, afile://document — and on a credentialed endpoint it should be rejected, exactly ascors-securityrequires. - A frame that carries no subject id, because the connection is scoped to one resource at handshake, is not missing per-frame authorization. Binding the subject once, at the upgrade, is the stronger design.
- Plaintext
ws://between services is acceptable where a service mesh terminates mTLS underneath it, because the mesh supplies a cryptographic peer identity. It is not acceptable because the traffic is inside a VPC — a private network is a location, not an authentication.
Context (for humans)
A WebSocket inverts the assumption every HTTP control is built on. HTTP gets a fresh authorization decision per request; a WebSocket makes one decision at the upgrade and then runs on it for as long as the socket lives. Everything in this skill follows from that: authenticate the handshake because there is only one, re-authorize each frame because the single decision goes stale, and bound the connection because it is an allocation nobody is forced to release.
The browser header limitation is worth stating plainly because it is the source of the
worst advice in this area. new WebSocket(url, protocols) accepts a URL and a
subprotocol list and nothing else — no headers. Faced with that, developers put the
JWT in the query string, and most guidance lets them. The ticket pattern exists because
it keeps the property that matters: what appears in the URL is worthless once used, so
the log line, the Referer and the history entry are all inert.
Frameworks that wrap the upgrade — Socket.IO, SignalR, Phoenix Channels — hide it well enough that the handshake stops looking like a request, which is why the Origin check and the handshake auth are the two things most often missing from generated code.
References
references/verifying-findings.md— confirm or refute a finding, then lock itreferences/limits-and-handshake.md— starting values for message size, rate, connection caps and heartbeats; backpressure; the ticket exchange end to end; and per-framework notes for Socket.IO, SignalR and Phoenix Channelsrules/websocket_hardening.json- OWASP WebSocket Security Cheat Sheet.
- CWE-1385 · CWE-770.
- RFC 6455.
Signals
- GitHub stars
- 22
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
websocket-security-shieldnet-360- Source
- github.com/shieldnet-360/secure-vibe