Who May Connect?
Where Did This Request Come From?
The server now refuses a scorekeeper without the token before 101, and it accepts a
Origin, and a page from somewhere else
Every handshake a browser sends carries an Origin header naming the site of the page that opened the connection, such as Origin: https://quiz.example. The page’s script cannot change it; the browser writes it.
That matters because of how browsers treat https://quiz.example, then, in another tab, visits a page on some other site. That page runs new WebSocket("wss://quiz.example/score"). The browser writes the handshake, and if the site allowed its cookie to travel with requests that other sites start, the browser attaches it. (Modern browsers hold most cookies back from such requests unless the site said otherwise; how a site says so is outside this course.) The quiz server sees a valid login and opens a scorekeeper connection, which the other site’s page now controls. This is called cross-site
Ordinary web requests have a defense built into the browser: a page can send a request to another site, but the browser does not let it read the answer. WebSockets are not covered by that rule. Once the connection is open, the page that opened it can read every message and send its own.
The scoreboard is not open to this as built, because its scorekeeper token is not a cookie. A token in the query
A page on another site opens a WebSocket to your server from a logged-in user's browser. Which credential does the browser add for it?
An allowlist
The defense is to read Origin and refuse pages you do not know. The library does it for you with one more serve:
serve(handler, "127.0.0.1", 0, origins=["https://quiz.example"])
With origins=, a handshake whose Origin is not in the 101, and so is a handshake with no Origin at all. The test it makes is a membership test, roughly this:
origin = request.headers.get("origin")
if origin is None or origin not in ["https://quiz.example"]:
... # answer 403 instead of 101
In a run on a local computer, with http://localhost:8000 as the only allowed Origin: http://evil.example got 403 with the body Failed to open a WebSocket connection: invalid Origin header: http://evil.example.; one with no Origin got 403 with Failed to open a WebSocket connection: missing Origin header.; and the listed origin was let through. A missing Origin is refused too, so a server with an allowlist also refuses scripts that send no Origin, such as the library’s Python client by default, unless None is in the list.
Scoreboard v3 does not install this check: its token is not a cookie, so a foreign page cannot ride on anything the browser adds. A server that identified scorekeepers by a cookie would need it.
That 403 is the one Lesson 2 promised. The server knows exactly where this request came from, and it still says no, so it is not asking for a credential. Sending the same request again will not help.
Your server allows only https://quiz.example, and a handshake arrives with Origin: http://evil.example. What does the library answer?
What Origin cannot do
Origin is honest only because browsers write it. Any program that is not a browser writes whatever Origin it likes, or none. The library’s Python client from Chapter 6 sends Origin: https://quiz.example from a laptop anywhere if you pass connect(url, origin="https://quiz.example"). So an allowlist protects the users of browsers from other sites’ pages. It does nothing to protect the server from scripts. The token still decides who may score, and an Origin check can only sit beside it, never replace it.
Why does an Origin allowlist not stop a Python script from scoring?
wss://
The hijacking page above used wss://quiz.example/score. wss:// is to ws:// what https:// is to http://: the same handshake and the same wss:// connection, and the server still has to check the token and, for browsers, the Origin.
This course runs its servers on ws:// and never sets up TLS. A real scoreboard on the internet would use wss://, because a token sent in the clear can be read by anyone on the way.
Scoreboard v3 has one piece left. A scorekeeper’s typo still closes its connection; the project turns those mistakes into answers and checks that the token appears in no reply.