socket.io icon indicating copy to clipboard operation
socket.io copied to clipboard

CORS works even if it is not enabled

Open alexandrmucha opened this issue 1 year ago • 10 comments

Describe the bug I am using socket.io in nuxt. Even though I didn't enable cors, it is possible to connect from a different origin than the one nuxt is running on. E.g. it runs on localhost:3000 and I can connect from localhost:8000. Is this a bug or is it my fault?

To Reproduce

Please fill the following code example:

Socket.IO server version: ^4.7.5

Server

import type { NitroApp } from "nitropack";
import { Server as Engine } from "engine.io";
import { Server } from "socket.io";
import { defineEventHandler } from "h3";
import { registerSocketHandlers } from "../sockets";

export default defineNitroPlugin((nitroApp: NitroApp) => {
  const engine = new Engine();
  const io = new Server();

  io.bind(engine);

  registerSocketHandlers(io);

  nitroApp.router.use("/socket.io/", defineEventHandler({
    handler(event) {
      engine.handleRequest(event.node.req, event.node.res);
      event._handled = true;
    },
    websocket: {
      open(peer) {
        const nodeContext = peer.ctx.node;
        const req = nodeContext.req;

        // @ts-expect-error private method
        engine.prepare(req);

        const rawSocket = nodeContext.req.socket;
        const websocket = nodeContext.ws;

        // @ts-expect-error private method
        engine.onWebSocket(req, rawSocket, websocket);
      }
    }
  }));
});

Socket.IO client version: ^4.7.5

Client

socket: io('http://localhost:3000', { transports: ['websocket', 'polling', 'flashsocket'] }),

Expected behavior It will not be possible to connect from an origin other than the one defined or on which nuxt is running.

Platform:

  • Device: PC
  • OS: Windows 10

Additional context Add any other context about the problem here.

alexandrmucha avatar Jul 25 '24 21:07 alexandrmucha

Hi!

CORS only applies to HTTP long-polling, but you use WebSocket first: ['websocket', 'polling', 'flashsocket']. If you switch back to ['polling', 'websocket'], it should fail as expected.

Note: the "flashsocket" transport does not exist anymore.

darrachequesne avatar Jul 25 '24 22:07 darrachequesne

Hi, thank you for your answer.

When I used the default transport method, the connection failed even though I defined the origin correctly. From what I read, it was caused by "the default transportation method is not always allowed by all servers": Stackoverflow

Can you confirm that the error was caused by an unsupported transport method and not an error with the cors configuration?

alexandrmucha avatar Jul 25 '24 22:07 alexandrmucha

The Stackoverflow post uses an old version of the socket.io package (2.2), you need to configure the cors option:

const io = new Server({
  cors: {
    origin: ["http://localhost:8000"]
  }
});

Reference: https://socket.io/docs/v4/handling-cors/

darrachequesne avatar Jul 26 '24 06:07 darrachequesne

Thats the problem. It doesnt work even though I set this origin correctly.

Access` to XMLHttpRequest at 'http://localhost:3000/socket.io/?EIO=4&transport=polling&t=P3kAbNS' from origin 'http://localhost:8000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. websocket.ts:6

const io = new Server({ cors: { origin: ["http://localhost:8000"] } });

i tried it also without http

alexandrmucha avatar Jul 26 '24 08:07 alexandrmucha

Hmm, that's weird... Could you please check if you are able to reach the Socket.IO server with:

$ curl -v -H "origin: http://localhost:3000" "http://localhost:8080/socket.io/?EIO=4&transport=polling"

It should return something like:

*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /socket.io/?EIO=4&transport=polling HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.81.0
> Accept: */*
> origin: http://localhost:3000
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Access-Control-Allow-Origin: http://localhost:3000
< Vary: Origin
< Content-Type: text/plain; charset=UTF-8
< Content-Length: 118
< cache-control: no-store
< Date: Fri, 26 Jul 2024 10:34:04 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< 
* Connection #0 to host localhost left intact
0{"sid":"IMkIs3bah6ZNcXwVAAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":20000,"maxPayload":1000000}

Reference: https://socket.io/docs/v4/troubleshooting-connection-issues/

darrachequesne avatar Jul 26 '24 10:07 darrachequesne

My server is running on port 3000 and I'm trying to connect from 8000, so I modified the command to include those addresses.

curl -v -H "origin: http://localhost:8000" "http://localhost:3000/socket.io/?EIO=4&transport=polling"

it returned me this:

* Host localhost:3000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:3000...
* Connected to localhost (::1) port 3000
> GET /socket.io/?EIO=4&transport=polling HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.7.1
> Accept: */*
> origin: http://localhost:8000
>
* Request completely sent off
< HTTP/1.1 200 OK
< content-type: text/plain; charset=UTF-8
< content-length: 118
< cache-control: no-store
< date: Fri, 26 Jul 2024 13:52:46 GMT
< connection: close
<
0{"sid":"iEWn6snaZTIUWBrqAAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":20000,"maxPayload":1000000}* Closing connection

alexandrmucha avatar Jul 26 '24 13:07 alexandrmucha

and it returns the same thing when I try to set the origin to not allowed in the command

alexandrmucha avatar Jul 26 '24 14:07 alexandrmucha

Also I dont see any cors header in network tab in devtools

alexandrmucha avatar Jul 27 '24 08:07 alexandrmucha

@darrachequesne I tried adding the cors header manually and it works then, could there be a problem with adding the headers? Can you please check my nuxt socket.io plugin code to see if I have a misconfigured server? Or is it possible that there is a bug in socket.io itself?

import type { NitroApp } from "nitropack";
import { Server as Engine } from "engine.io";
import { Server } from "socket.io";
import { defineEventHandler } from "h3";
import { registerSocketHandlers } from "../sockets";

export default defineNitroPlugin((nitroApp: NitroApp) => {
  const engine = new Engine();
  
  const io = new Server({
    cors: {
      origin: "http://localhost:8000",
    }
  });

  io.bind(engine);

  registerSocketHandlers(io);

  nitroApp.router.use("/socket.io/", defineEventHandler({
    handler(event) {
      
      // Here I manually added the cors origin header
      event.node.res.setHeader("Access-Control-Allow-Origin", "http://localhost:8000");

      engine.handleRequest(event.node.req, event.node.res);
      event._handled = true;
    },
    websocket: {
      open(peer) {
        const nodeContext = peer.ctx.node;
        const req = nodeContext.req;

        // @ts-expect-error private method
        engine.prepare(req);

        const rawSocket = nodeContext.req.socket;
        const websocket = nodeContext.ws;

        // @ts-expect-error private method
        engine.onWebSocket(req, rawSocket, websocket);
      }
    }
  }));
});

alexandrmucha avatar Jul 27 '24 09:07 alexandrmucha

To summarize, while both configurations handle CORS, they do so at different stages and for different purposes. The origin option in the Server constructor controls whether the server will accept connections from a given origin, and the Access-Control-Allow-Origin header in the response ensures that the browser will accept the response from the server.

So they are not same things and socket.io initaly not offering to send header to browser when you give origin

semiharslanait avatar Aug 01 '24 12:08 semiharslanait

Hey, just to add to this, I am experiencing the same behaviour.

I created a very simple demo project to illustrate (if you don't want to click on the link just look at my public repos): https://github.com/EMDevelop/socket-io-cors-issue

In this demo, the servers are as follows:

  • Client: http://localhost:3000
  • Server: http://localhost:8000
  • Server Side CORS: http://localhost:9999

My chrome browser does not block the request, despite the origin differing:

For the time being, we're able to use the allowRequest() option to block requests server side, but previously we were relying on the cors flag which doesn't seem to work.

From reading around, it might be because Websocket connections don't use pre-flight checks, the connection is made and upgraded to a Websocket connection in one go.

I'm commenting here because it is a little misleading that the cors flag is available, but is not enforced, so it could lead others into a false sense of security.

Other info incase you want to recreate exactly, although I'd assume you'll get this behaviour on other versions:

Chrome version: 140.0.7339.133 (Official Build) (arm64)
Node: v20.17.0
socket.io: 4.7.4

Thanks, Ed

EMDevelop avatar Sep 17 '25 10:09 EMDevelop

Hi!

I've updated the documentation to highlight two important caveats:

  • CORS only applies to browsers

Even with proper CORS setup, an attacker can still run a script on his machine or on a VM and reach your website. Native applications are not covered either.

  • CORS only applies to HTTP long-polling

WebSocket connections are not subject to CORS restrictions.

Reference: https://socket.io/docs/v4/handling-cors/

Hope it's clearer now!

darrachequesne avatar Sep 19 '25 15:09 darrachequesne