{"id":14638131,"url":"https://github.com/0ang3el/websocket-smuggle","last_synced_at":"2025-04-05T21:11:13.463Z","repository":{"id":44400964,"uuid":"219180978","full_name":"0ang3el/websocket-smuggle","owner":"0ang3el","description":"Issues with WebSocket reverse proxying allowing to smuggle HTTP requests","archived":false,"fork":false,"pushed_at":"2024-08-15T07:52:53.000Z","size":1455,"stargazers_count":353,"open_issues_count":3,"forks_count":56,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-03-29T20:08:41.495Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/0ang3el.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-11-02T16:27:34.000Z","updated_at":"2025-03-28T00:19:52.000Z","dependencies_parsed_at":"2024-12-18T17:10:36.731Z","dependency_job_id":"2c69e409-8139-4025-aad3-88a0add36ddd","html_url":"https://github.com/0ang3el/websocket-smuggle","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0ang3el%2Fwebsocket-smuggle","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0ang3el%2Fwebsocket-smuggle/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0ang3el%2Fwebsocket-smuggle/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0ang3el%2Fwebsocket-smuggle/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/0ang3el","download_url":"https://codeload.github.com/0ang3el/websocket-smuggle/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247399885,"owners_count":20932880,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-09-10T02:01:43.798Z","updated_at":"2025-04-05T21:11:13.434Z","avatar_url":"https://github.com/0ang3el.png","language":"Python","funding_links":[],"categories":["Python","\u003ca name=\"common_websocket_weaknesses\"\u003e\u003c/a\u003eCommon WebSocket Weaknesses"],"sub_categories":["Reverse Proxy Bypass using Upgrade Header"],"readme":"## Smuggling HTTP requests over fake WebSocket connection\n\n### 0. WebSocket protocol\n\nWebSocket communication consists of two parts: handshake and data transfer. Firstly TCP or TLS connection is established with the backend, next handshake part takes place over established connection. And finally WebSocket frames are transfered through **the same TCP or TLS connection**.\n\nWebSocket protocol is described in [RFC 6455](https://tools.ietf.org/html/rfc6455).\n\nLet's observe handshake part that's done over HTTP protocol of version 1.1 (as described in RFC 6455). Client sends GET request (aka **Upgrade request**) to the backend with the set of special HTTP headers: `Upgrade`, `Sec-WebSocket-Version`, `Sec-WebSocket-Key`.\n\n```\nGET /socket.io/?EIO=3\u0026transport=websocket HTTP/1.1\nHost: websocket.example.com\nSec-WebSocket-Version: 13\nOrigin: http://websocket.example.com\nSec-WebSocket-Key: nf6dB8Pb/BLinZ7UexUXHg==\nConnection: keep-alive, Upgrade\nPragma: no-cache\nCache-Control: no-cache\nUpgrade: websocket\n\n```\n\nWhere `Sec-WebSocket-Version` contains WebSocket protocol version (most of the times only version `13` is supported). Header `Sec-WebSocket-Key` contains random nonce encoded as base64. And `Upgrade` header contain value `websocket`.\n\nBackend should responds with status code `101` and acknowledge nonce sent by client. Backend should compute confirmation value using nonce from client and send it inside `Sec-WebSocket-Accept` HTTP Header.\n\n```\nHTTP/1.1 101 Switching Protocols\nUpgrade: websocket\nConnection: Upgrade\nSec-WebSocket-Accept: xUFfT1/AIvZnGdK/XSbIr7zQO10=\n\n```\n\nAfter successfull handshake client or backend can transfer data using WebSocket frames. There are different types of frames. Each frame has field `opcode` to indicate its type.\n\n**What's important that client must do so-called client-to-server masking!** Masking is done to mitigate potential attacks on infrastructure that proxies WebSocket connections between client and backend. As pointed in RFC 6455, there was a [research](http://www.adambarth.com/papers/2011/huang-chen-barth-rescorla-jackson.pdf) showing cache poisoning attacks against proxies in case client doesn't perform client-to-server masking. We will see shortly that it also leads to smuggling attacks.\n\n### 1. WebSocket connection reverse proxying\n\nIn real web applications client and backend communicate through some reverse proxy. Currently most of web servers, load balancer, http proxies allow to proxy WebSocket traffic.\n\nLet's observe how WebSocket communication should be done in case reverse proxy is envolved. Below an ideal picture is described.\n\nOn the first step client sends Upgrade request to the reverse proxy. Proxy checks that incoming request is indeed Upgrade request by checking HTTP method, `Upgrade` header, version from `Sec-WebSocket-Version` header, presence of `Sec-WebSocket-Key` header, etc. If request is a correct Upgrade request proxy translates it to the backend.\n\n![](img/1-1.png)\n\nOn the second step backend answers reverse proxy with HTTP response that has status code `101`. Response also has `Upgrade` and `Sec-WebSocket-Accept` headers. Reverse proxy should check that backend indeed is ready to establish WebSocket connection by checking status code and other headers. If everyhing is correct reverse proxy translates response from backend to the client.\n\n![](img/1-2.png)\n\nOn the third step proxy doesn't close TCP or TLS connection between client and backend and they both agreed to use this connection for WebSocket communication. Since than client and backend can send WebSocket frames back and forth. Proxy should check that client sends masked WebSocket frames.\n\n![](img/1-3.png)\n\n### 2. Smuggling techniques\n\nIn reality, however, reverse proxies can behave differently and do not adhere fully to RFC 6445. This leads to smuggling attacks.\n\n### 2.1 Scenario 1\n\nLet's observe first scenario. We have backend that exposes public WebSocket API and also has internal REST API not available from outside. Malicious client wants to access internal REST API.\n\n![](img/2-1.png)\n\nOn the first step client sends Upgrade request to reverse proxy but with wrong protocol version inside header `Sec-WebSocket-Version`. Proxy doesn't validate `Sec-WebSocket-Version` header and thinks that Upgrade request is correct. Further it translates request to the backend.\n\n![](img/2-2.png)\n\nOn the second step backend sends response with status code `426` because protocol version is incorrect inside header `Sec-WebSocket-Version`. However, reverse proxy doesn't check enough response from backend (including status code) and thinks that backend is ready for WebSocket communication. Further it translates request to the client.\n\n![](img/2-3.png)\n\nFinally, reverse proxy thinks that WebSocket connection is established between client and backend. In reality there is no WebSocket connection - backend refused Upgrade request. At the same time, proxy keeps TCP or TLS connection between client and backend in open state. **Client can easily access private REST API by sending HTTP request over the connection.**\n\n![](img/2-4.png)\n\nIt was found that following reverse proxies are affected:\n* Varnish - team refused to fix described issue.\n* Envoy proxy 1.8.0 (or older) - in newer versions upgrade mechanism has been changed.\n* Others - TBA.\n\nFirst challenge utilized described scenario. To solve it you should access flag from localhost and port 5000. Backend is based on `Flask` with `eventlen` module as web server. Module `Flask-SocketIO` is used for WebSocket support. Module `Flask-Restful` is used for building REST API.\n\n### 2.2 Scenario 2\n\nThe majority of reverse proxies (e.g. NGINX) check status code from backend during handshake part. This makes attack harder but not impossible.\n\nLet's observe second scenario. We have backend that exposes public WebSocket API and public REST API for health checking and also has internal REST API not available from outside. Malicious client wants to access internal REST API. NGINX is used as reverse proxy. WebSocket API is available on path `/api/socket.io/` and healthcheck API on path `/api/health`.\n\n![](img/3-1.png)\n\nHealthcheck API is invoked by sending POST request, parameter with name `u` controls URL. Backend reaches external resource and returns status code back to the client.\n\n![](img/3-2.png)\n\nOn the first step client sends POST request to invoke healthcheck API but with additional HTTP header `Upgrade: websocket`. NGINX thinks that it's a normal Upgrade request, it looks only for `Upgrade` header skipping other parts of the request. Further proxy translates request to the backend.\n\n![](img/3-3.png)\n\nOn the second step backend invokes healtcheck API. It reaches external resource controlled by malicious users that returns HTTP response with status code `101`. Backend translates that response to the reverse proxy. Since NGINX validates only status code it will think that backend is ready for WebSocket communication. Further it translates request to the client.\n\n![](img/3-4.png)\n\nFinally, NGINX thinks that WebSocket connection is established between client and backend. In reality there is no WebSocket connection - healthcheck REST API was invoked on backend. At the same time, reverse proxy keeps TCP or TLS connection between client and backend in open state. **Client can easily access private REST API by sending HTTP request over the connection.**\n\n![](img/3-5.png)\n\nThe majority of reverse proxies should be affected by that scenario. However, exploitation requires existence of external SSRF vulnerability (usually considered low-severity issue).\n\nSecond challenge utilized described scenario. To solve it you should access flag from localhost and port 5000. Backend is based on `Flask` with `eventlen` module as web server. Module `Flask-SocketIO` is used for WebSocket support. Module `Flask-Restful` is used for building REST API.\n\nNGINX is used as a reverse proxy with the following configuration allowing WebSocket communication for path `/api/public`.\n\n```\nlocation ~ ^/api/public {\n    proxy_pass http://127.0.0.1:5000;\n    proxy_http_version 1.1;\n    proxy_set_header Upgrade $http_upgrade;\n    proxy_set_header Connection \"upgrade\";\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F0ang3el%2Fwebsocket-smuggle","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F0ang3el%2Fwebsocket-smuggle","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F0ang3el%2Fwebsocket-smuggle/lists"}