Konrad Kowalski (rootsher)Principal Platform & Reliability Architect111111100011111010000010010110101001110011010000

The Browser Does Not Give JavaScript Full Control over HTTP

date
category
Frontend
also in
Application Security · Backend
reading
2 min / 370 words

fetch() looks like a request generator:

js
fetch(url, options);

But the frontend does not get a raw HTTP socket.

It gets a controlled browser interface.

The browser adds state of its own

This code:

js
fetch("/api/me");

can lead to a request carrying cookies, negotiation headers and other fields set by the user agent.

JavaScript does not have to provide them explicitly.

So the model is:

text
application intent
      |
      v
Fetch API
      |
      v
browser policy + browser state
      |
      v
HTTP request

Not every header is ours to set

The browser blocks or controls some headers, because JavaScript running on a page cannot be granted the full ability to impersonate the user agent or interfere with the transport.

That is why a request from a browser is not the equivalent of:

bash
curl ...

curl is an HTTP client controlled by the user.

Page code runs inside the browser's security model.

That is a fundamental difference.

Cookies are not an ordinary application header either

The frontend should not build this:

js
headers: {
  Cookie: "session=..."
}

The browser manages the cookie store and decides which cookies qualify for a given request.

Fetch has an option:

js
fetch(url, {
  credentials: "same-origin"
});

By default credentials are attached for same-origin requests.

We can also use:

js
credentials: "omit"
credentials: "include"

But even include does not bypass rules such as SameSite.

Cross-origin can add a request we never wrote

This code:

js
fetch("https://api.example.net/users", {
  method: "PUT",
  headers: {
    "Content-Type": "application/json"
  },
  body: "{}"
});

can cause an earlier request:

http
OPTIONS /users

that is, a CORS preflight.

The frontend wrote one fetch().

The Network panel shows two requests.

Another example of:

text
JavaScript operation
!=
one network request

CORS gets its own treatment in the security series. What matters here is the model: the browser can introduce an extra step before the actual request.

The cache also sits between the code and the network

This code:

js
await fetch("/logo.svg");

does not guarantee that any data travels over the network.

The browser may hold a fresh response in its cache.

From the application's point of view we still performed a fetch.

From the origin server's point of view the request may never arrive.

text
fetch()
  |
  v
browser cache?
  ├── hit -> Response
  └── miss -> network

Caching deserves a series of its own. Here one thing matters: fetch() is a platform operation, not a direct call to the network.

The same goes for connections

Even when the request does reach the network, the browser does not have to create a new connection for it.

It can reuse an existing one.

And connection reuse is exactly what makes the differences between HTTP/1.1, HTTP/2 and HTTP/3 understandable.