This commit is contained in:
Ilya Kantor 2019-08-10 21:19:35 +03:00
parent 0e08048f81
commit 4ac9bec212
6 changed files with 87 additions and 128 deletions

View file

@ -2,7 +2,7 @@ We need `Origin`, because sometimes `Referer` is absent. For instance, when we `
The [Content Security Policy](http://en.wikipedia.org/wiki/Content_Security_Policy) may forbid sending a `Referer`.
As we'll see, `fetch` also has options that prevent sending the `Referer` and even allow to change it (within the same site).
As we'll see, `fetch` has options that prevent sending the `Referer` and even allow to change it (within the same site).
By specification, `Referer` is an optional HTTP-header.

View file

@ -135,7 +135,7 @@ If a request is cross-origin, the browser always adds `Origin` header to it.
For instance, if we request `https://anywhere.com/request` from `https://javascript.info/page`, the headers will be like:
```
```http
GET /request
Host: anywhere.com
*!*
@ -155,7 +155,7 @@ The browser plays the role of a trusted mediator here:
![](xhr-another-domain.svg)
Here's an example of a permissive server response:
```
```http
200 OK
Content-Type:text/html; charset=UTF-8
*!*
@ -186,7 +186,7 @@ To grant JavaScript access to any other response header, the server must send `
For example:
```
```http
200 OK
Content-Type:text/html; charset=UTF-8
Content-Length: 12345
@ -209,8 +209,8 @@ So, to avoid misunderstandings, any "non-simple" request -- that couldn't be don
A preflight request uses method `OPTIONS`, no body and two headers:
- `Access-Control-Request-Method` header has the method of a non-simple request.
- `Access-Control-Request-Headers` header provides a comma-separated list of non-simple HTTP-headers.
- `Access-Control-Request-Method` header has the method of an the non-simple request.
- `Access-Control-Request-Headers` header provides a comma-separated list of its non-simple HTTP-headers.
If the server agrees to serve the requests, then it should respond with empty body, status 200 and headers:
@ -234,14 +234,14 @@ let response = await fetch('https://site.com/service.json', {
There are three reasons why the request is not simple (one is enough):
- Method `PATCH`
- `Content-Type` is not one of: `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`.
- `Content-Type` is not one of: `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`.
- "Non-simple" `API-Key` header.
### Step 1 (preflight request)
Prior to sending our request, the browser, on its own, sends a preflight request that looks like this:
Prior to sending such request, the browser, on its own, sends a preflight request that looks like this:
```
```http
OPTIONS /service.json
Host: site.com
Origin: https://javascript.info
@ -264,26 +264,26 @@ The server should respond with status 200 and headers:
That allows future communication, otherwise an error is triggered.
If the server expects other methods and headers in the future, makes sense to allow them in advance by adding to the list:
If the server expects other methods and headers in the future, it makes sense to allow them in advance by adding to the list:
```
```http
200 OK
Access-Control-Allow-Methods: PUT,PATCH,DELETE
Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control
Access-Control-Max-Age: 86400
```
Now the browser can see that `PATCH` is in the list of allowed methods, and both headers are in the list too, so it sends out the main request.
Now the browser can see that `PATCH` in `Access-Control-Allow-Methods` and `Content-Type,API-Key` are in the list `Access-Control-Allow-Headers`, so it sends out the main request.
Besides, the preflight response is cached for time, specified by `Access-Control-Max-Age` header (86400 seconds, one day), so subsequent requests will not cause a preflight. Assuming that they fit the allowances, they will be sent directly.
Besides, the preflight response is cached for time, specified by `Access-Control-Max-Age` header (86400 seconds, one day), so subsequent requests will not cause a preflight. Assuming that they fit the cached allowances, they will be sent directly.
### Step 3 (actual request)
When the preflight is successful, the browser now makes the real request. Here the flow is the same as for simple requests.
When the preflight is successful, the browser now makes the main request. The algorithm here is the same as for simple requests.
The real request has `Origin` header (because it's cross-origin):
The main request has `Origin` header (because it's cross-origin):
```
```http
PATCH /service.json
Host: site.com
Content-Type: application/json
@ -293,14 +293,19 @@ Origin: https://javascript.info
### Step 4 (actual response)
The server should not forget to add `Access-Control-Allow-Origin` to the response. A successful preflight does not relieve from that:
The server should not forget to add `Access-Control-Allow-Origin` to the main response. A successful preflight does not relieve from that:
```
```http
Access-Control-Allow-Origin: https://javascript.info
```
Now everything's correct. JavaScript is able to read the full response.
Then JavaScript is able to read the main server response.
```smart
Preflight request occurs "behind the scenes", it's invisible to JavaScript.
JavaScript only gets the response to the main request or an error if there's no server permission.
```
## Credentials
@ -308,15 +313,15 @@ A cross-origin request by default does not bring any credentials (cookies or HTT
That's uncommon for HTTP-requests. Usually, a request to `http://site.com` is accompanied by all cookies from that domain. But cross-origin requests made by JavaScript methods are an exception.
For example, `fetch('http://another.com')` does not send any cookies, even those that belong to `another.com` domain.
For example, `fetch('http://another.com')` does not send any cookies, even those (!) that belong to `another.com` domain.
Why?
That's because a request with credentials is much more powerful than an anonymous one. If allowed, it grants JavaScript the full power to act and access sensitive information on behalf of a user.
That's because a request with credentials gives much more powerful than without them. If allowed, it grants JavaScript the full power to act on behalf of the user and access sensitive information using their credentials.
Does the server really trust pages from `Origin` that much? Then it must explicitly allow requests with credentials with an additional header.
Does the server really trust the script that much? Then it must explicitly allow requests with credentials with an additional header.
To send credentials, we need to add the option `credentials: "include"`, like this:
To send credentials in `fetch`, we need to add the option `credentials: "include"`, like this:
```js
fetch('http://another.com', {
@ -326,22 +331,21 @@ fetch('http://another.com', {
Now `fetch` sends cookies originating from `another.com` with out request to that site.
If the server wishes to accept the request with credentials, it should add a header `Access-Control-Allow-Credentials: true` to the response, in addition to `Access-Control-Allow-Origin`.
If the server agrees to accept the request *with credentials*, it should add a header `Access-Control-Allow-Credentials: true` to the response, in addition to `Access-Control-Allow-Origin`.
For example:
```
```http
200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Credentials: true
```
Please note: `Access-Control-Allow-Origin` is prohibited from using a star `*` for requests with credentials. There must be exactly the origin there, like above. That's an additional safety measure, to ensure that the server really knows who it trusts.
Please note: `Access-Control-Allow-Origin` is prohibited from using a star `*` for requests with credentials. There must be exactly the origin there, like above. That's an additional safety measure, to ensure that the server really knows who it trusts to make such requests.
## Summary
Networking methods split cross-origin requests into two kinds: "simple" and all the others.
From the browser point of view, there are to kinds of cross-origin requests: "simple" and all the others.
[Simple requests](http://www.w3.org/TR/cors/#terminology) must satisfy the following conditions:
- Method: GET, POST or HEAD.
@ -353,7 +357,7 @@ Networking methods split cross-origin requests into two kinds: "simple" and all
The essential difference is that simple requests were doable since ancient times using `<form>` or `<script>` tags, while non-simple were impossible for browsers for a long time.
So, practical difference is that simple requests are sent right away, with `Origin` header, but for other ones the browser makes a preliminary "preflight" request, asking for permission.
So, the practical difference is that simple requests are sent right away, with `Origin` header, while for the other ones the browser makes a preliminary "preflight" request, asking for permission.
**For simple requests:**
@ -364,21 +368,13 @@ So, practical difference is that simple requests are sent right away, with `Orig
- `Access-Control-Allow-Origin` to same value as `Origin`
- `Access-Control-Allow-Credentials` to `true`
Additionally, if JavaScript wants to access non-simple response headers:
- `Cache-Control`
- `Content-Language`
- `Content-Type`
- `Expires`
- `Last-Modified`
- `Pragma`
...Then the server should list the allowed ones in `Access-Control-Expose-Headers` header.
Additionally, to grant JavaScript access to any response headers except `Cache-Control`, `Content-Language`, `Content-Type`, `Expires`, `Last-Modified` or `Pragma`, the server should list the allowed ones in `Access-Control-Expose-Headers` header.
**For non-simple requests, a preliminary "preflight" request is issued before the requested one:**
- → The browser sends `OPTIONS` request to the same url, with headers:
- `Access-Control-Request-Method` has requested method.
- `Access-Control-Request-Headers` lists non-simple requested headers
- `Access-Control-Request-Headers` lists non-simple requested headers.
- ← The server should respond with status 200 and headers:
- `Access-Control-Allow-Methods` with a list of allowed methods,
- `Access-Control-Allow-Headers` with a list of allowed headers,

View file

@ -1,31 +0,0 @@
<!doctype html>
<script>
async function init() {
const response = await fetch('long.txt');
const reader = response.body.getReader();
const contentLength = +response.headers.get('Content-Length');
let receivedLength = 0;
while(true) {
const chunk = await reader.read();
if (chunk.done) {
console.log("done!");
break;
}
receivedLength += chunk.value.length;
console.log(`${receivedLength}/${contentLength} received`)
}
let result = await response.text();
console.log(result);
//const chunkCount = await read(reader);
//console.log(`Finished! Received ${chunkCount} chunks.`);
}
init();
</script>

View file

@ -1,31 +0,0 @@
const Koa = require('koa');
const app = new Koa();
const Router = require('koa-router');
let router = new Router();
router.get('/script', async (ctx) => {
let callback = ctx.query.callback;
if (!callback) {
ctx.throw(400, 'Callback required!');
}
ctx.type = 'application/javascript';
ctx.body = `${callback}({
temperature: 25,
humidity: 78
})`;
});
app
.use(router.routes())
.use(router.allowedMethods());
if (!module.parent) {
http.createServer(app.callback()).listen(8080);
} else {
exports.accept = app.callback();
}

View file

@ -3,7 +3,7 @@
So far, we know quite a bit about `fetch`.
Now let's see the rest of API, to cover all its abilities.
Let's see the rest of API, to cover all its abilities.
Here's the full list of all possible `fetch` options with their default values (alternatives in comments):
@ -11,11 +11,13 @@ Here's the full list of all possible `fetch` options with their default values (
let promise = fetch(url, {
method: "GET", // POST, PUT, DELETE, etc.
headers: {
// the content type header value is usually auto-set depending on the request body
// the content type header value is usually auto-set
// depending on the request body
"Content-Type": "text/plain;charset=UTF-8"
},
body: undefined // string, FormData, Blob, BufferSource, or URLSearchParams
referrer: "about:client", // or "" to send no Referer header, or an url from the current origin
referrer: "about:client", // or "" to send no Referer header,
// or an url from the current origin
referrerPolicy: "no-referrer-when-downgrade", // no-referrer, origin, same-origin...
mode: "cors", // same-origin, no-cors
credentials: "same-origin", // omit, include
@ -34,15 +36,15 @@ We fully covered `method`, `headers` and `body` in the chapter <info:fetch>.
The `signal` option is covered in <info:fetch-abort>.
Now let's explore the rest of options.
Now let's explore the rest of capabilities.
## referrer, referrerPolicy
These options govern how `fetch` sets HTTP `Referer` header.
That header contains the url of the page that made the request. In most scenarios, it plays a very minor informational role, but sometimes, for security purposes, it makes sense to remove or shorten it.
Usually that header is set automatically and contains the url of the page that made the request. In most scenarios, it's not important at all, sometimes, for security purposes, it makes sense to remove or shorten it.
**The `referrer` option allows to set any `Referer` within the current origin) or disable it.**
**The `referrer` option allows to set any `Referer` within the current origin) or remove it.**
To send no referer, set an empty string:
```js
@ -67,48 +69,71 @@ fetch('/page', {
**The `referrerPolicy` option sets general rules for `Referer`.**
Requests are split into 3 types:
1. Request to the same origin.
2. Request to another origin.
3. Request from HTTPS to HTTP (from safe to unsafe protocol).
Unlike `referrer` option that allows to set the exact `Referer` value, `referrerPolicy` tells the browser general rules for each request type.
Possible values are described in the [Referrer Policy specification](https://w3c.github.io/webappsec-referrer-policy/):
- **`"no-referrer-when-downgrade"`** -- default value: `Referer` is sent always, unless we send a request from HTTPS to HTTP (to less secure protocol).
- **`"no-referrer-when-downgrade"`** -- the default value: full `Referer` is sent always, unless we send a request from HTTPS to HTTP (to less secure protocol).
- **`"no-referrer"`** -- never send `Referer`.
- **`"origin"`** -- only send the origin in `Referer`, not the full page URL, e.g. `http://site.com` instead of `http://site.com/path`.
- **`"origin-when-cross-origin"`** -- send full `Referer` to the same origin, but only the origin part for cross-origin requests.
- **`"origin"`** -- only send the origin in `Referer`, not the full page URL, e.g. only `http://site.com` instead of `http://site.com/path`.
- **`"origin-when-cross-origin"`** -- send full `Referer` to the same origin, but only the origin part for cross-origin requests (as above).
- **`"same-origin"`** -- send full `Referer` to the same origin, but no referer for for cross-origin requests.
- **`"strict-origin"`** -- send only origin, don't send `Referer` for HTTPS→HTTP requests.
- **`"strict-origin-when-cross-origin"`** -- for same-origin send full `Referer`, for cross-origin send only origin, unless it's HTTPS→HTTP request, then send nothing.
- **`"unsafe-url"`** -- always send full url in `Referer`.
- **`"unsafe-url"`** -- always send full url in `Referer`, even for HTTPS→HTTP requests.
Here's a table with all combinations:
| Value | To same origin | To another origin | HTTPS→HTTP |
|-------|----------------|-------------------|------------|
| `"no-referrer"` | - | - | - |
| `"no-referrer-when-downgrade"` or `""` (default) | full | full | - |
| `"origin"` | origin | origin | origin |
| `"origin-when-cross-origin"` | full | origin | origin |
| `"same-origin"` | full | - | - |
| `"strict-origin"` | origin | origin | - |
| `"strict-origin-when-cross-origin"` | full | origin | - |
| `"unsafe-url"` | full | full | full |
Let's say we have an admin zone with URL structure that shouldn't be known from outside of the site.
If we send a cross-origin `fetch`, then by default it sends the `Referer` header with the full url of our page (except when we request from HTTPS to HTTP, then no `Referer`).
If we send a `fetch`, then by default it sends the `Referer` header with the full url of our page (except when we request from HTTPS to HTTP, then no `Referer`).
E.g. `Referer: https://javascript.info/admin/secret/paths`.
If we'd like to totally hide the referrer:
If we'd like other websites know only the origin part, not URL-path, we can set the option:
```js
fetch('https://another.com/page', {
referrerPolicy: "no-referrer" // no Referer, same effect as referrer: ""
referrerPolicy: "origin-when-cross-origin" // Referer: https://javascript.info
});
```
Otherwise, if we'd like the remote side to see only the domain where the request comes from, but not the full URL, we can send only the "origin" part of it:
We can put it to all `fetch` calls, maybe integrate into JavaScript library of our project that does all requests and uses `fetch` inside.
```js
fetch('https://another.com/page', {
referrerPolicy: "strict-origin" // Referer: https://javascript.info
});
Its only difference compared to the default behavior is that for requests to another origin `fetch` only sends the origin part of the URL. For requests to our origin we still get the full `Referer` (maybe useful for debugging purposes).
```smart header="Referrer policy is not only for `fetch`"
Referrer policy, described in the [specification](https://w3c.github.io/webappsec-referrer-policy/), is not just for `fetch`, but more global.
In particular, it's possible to set the default policy for the whole page using `Referrer-Policy` HTTP header, or per-link, with `<a rel="noreferrer">`.
```
## mode
The `mode` option serves as a safe-guard that prevents cross-origin requests:
The `mode` option is a safe-guard that prevents occasional cross-origin requests:
- **`"cors"`** -- the default, cross-origin requests are allowed, as described in <info:fetch-crossorigin>,
- **`"same-origin"`** -- cross-origin requests are forbidden,
- **`"no-cors"`** -- only simple cross-origin requests are allowed.
That may be useful in contexts when the fetch url comes from 3rd-party, and we want a "power off switch" to limit cross-origin capabilities.
This option may be useful when the URL comes from 3rd-party, and we want a "power off switch" to limit cross-origin capabilities.
## credentials
@ -124,11 +149,11 @@ By default, `fetch` requests make use of standard HTTP-caching. That is, it hono
The `cache` options allows to ignore HTTP-cache or fine-tune its usage:
- **`"default"`** -- `fetch` uses standard HTTP-cache rules and headers;
- **`"no-store"`** -- totally ignore HTTP-cache, this mode becomes the default if we set a header `If-Modified-Since`, `If-None-Match`, `If-Unmodified-Since`, `If-Match`, or `If-Range`;
- **`"reload"`** -- don't take the result from HTTP-cache (if any), but populate cache with the response (if response headers allow);
- **`"no-cache"`** -- create a conditional request if there is a cached response, and a normal request otherwise. Populate HTTP-cache with the response;
- **`"force-cache"`** -- use a response from HTTP-cache, even if it's stale. If there's no response in HTTP-cache, make a regular HTTP-request, behave normally;
- **`"default"`** -- `fetch` uses standard HTTP-cache rules and headers,
- **`"no-store"`** -- totally ignore HTTP-cache, this mode becomes the default if we set a header `If-Modified-Since`, `If-None-Match`, `If-Unmodified-Since`, `If-Match`, or `If-Range`,
- **`"reload"`** -- don't take the result from HTTP-cache (if any), but populate cache with the response (if response headers allow),
- **`"no-cache"`** -- create a conditional request if there is a cached response, and a normal request otherwise. Populate HTTP-cache with the response,
- **`"force-cache"`** -- use a response from HTTP-cache, even if it's stale. If there's no response in HTTP-cache, make a regular HTTP-request, behave normally,
- **`"only-if-cached"`** -- use a response from HTTP-cache, even if it's stale. If there's no response in HTTP-cache, then error. Only works when `mode` is `"same-origin"`.
## redirect
@ -147,13 +172,13 @@ The `integrity` option allows to check if the response matches the known-ahead c
As described in the [specification](https://w3c.github.io/webappsec-subresource-integrity/), supported hash-functions are SHA-256, SHA-384, and SHA-512, there might be others depending on a browser.
For example, we're downloading a file, and we know that it's SHA-256 checksum is "abc" (a real checksum is longer, of course).
For example, we're downloading a file, and we know that it's SHA-256 checksum is "abcdef" (a real checksum is longer, of course).
We can put it in the `integrity` option, like this:
```js
fetch('http://site.com/file', {
integrity: 'sha256-abd'
integrity: 'sha256-abcdef'
});
```

Binary file not shown.