CORS works even if it is not enabled
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.
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.
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?
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/
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
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/
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
and it returns the same thing when I try to set the origin to not allowed in the command
Also I dont see any cors header in network tab in devtools
@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);
}
}
}));
});
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
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
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!