0%

Who May Connect?

Where Did This Request Come From?

The server now refuses a scorekeeper without the token before 101, and it accepts a from any web page that asks. Lesson 1 listed the headers a browser writes for a page. One of them answers a question the token does not: which page made the browser open this connection?

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 . A browser sends the cookies it holds for a site with every request to that site, and a WebSocket handshake is a request. Suppose a quiz server identified its scorekeepers by a login cookie instead of a token. A scorekeeper logs in at 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 hijacking.

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 is part of the the page chose, so a page on another site would have to know the token to send it; the browser never adds it on its own. That is one reason this course puts the token in the query and not in a cookie.

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 to serve:

serve(handler, "127.0.0.1", 0, origins=["https://quiz.example"])

With origins=, a handshake whose Origin is not in the is refused before 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 , a handshake with 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 , carried inside a connection, the encryption you met in Networks with Python and in with Python’s HTTPS. TLS gives two things. Nobody on the network path can read the traffic, so a token in the query string or in a header is hidden from them. And the client can check that it is talking to the server whose name it asked for. TLS changes nothing about who may connect: anyone can open a wss:// connection, and the server still has to check the token and, for browsers, the Origin.

This course runs its servers on with 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.