# Client side security - My Practice
К решению прикладывайте скриншоты и код, который добавили в `client-side-playground`.
Можете оставить комментарии к результатам, например, почему произошла ошибка.
## 1. SOP
### 1.1 `iframe` / window
Вместо `iframe` можно открывать новое окно.
- [ ] Открыть `iframe` со страницей на **том же** `Origin`.
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Page with Frame</title>
<script>
function onShowFrameButtonClick(){
alert(document.getElementsByTagName("iframe")[0].contentDocument.documentElement.outerHTML);
}
</script>
</head>
<body>
This page contains iframe:
<br>
<form>
<input type="button" value="Show IFrame Content" onclick="onShowFrameButtonClick();">
</form>
<br>
<iframe src="frame1.html"/> <br>
</body>
</html>
```
- [ ] Прочитать содержимое `iframe` из Javascript.

- [ ] Открыть `iframe` со страницей на **другом** `Origin`.
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Page with Frame</title>
<script>
function onShowFrameButtonClick(){
alert(document.getElementsByTagName("iframe")[0].contentDocument.documentElement.outerHTML);
}
</script>
</head>
<body>
This page contains iframe:
<br>
<form>
<input type="button" value="Show IFrame Content" onclick="onShowFrameButtonClick();">
</form>
<br>
<iframe src="https://a.hu.io/ui/frame1.html"/> <br>
</body>
</html>
```
- [ ] Прочитать содержимое `iframe` из Javascript.

Это запрещено Same Origin Policy
### 1.2 inherited origins
- [ ] С основной страницы открыть окно `about:blank` и посмотреть `Origin` (`document.domain`).
```
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>Task 1.2</title>
</head>
<body>
<input type="button" onclick="let w = window.open('about:blank'); alert(w.document.domain);" value="Open Empty Window" ></input>
</body>
</html>
```

- [ ] С основной страницы открыть окно со схемой `data:` и посмотреть `Origin`.
Не удалось открыть новое окно со схемой `data:`: всё, что я ни пробовал, либо открывает вкладку со страницей about:blank, либо вообще не открывает ничего
Например, это происходит при нажатии кнопки Open Data Window на этой страничке:
```
!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>Task 1.2</title>
<script>
var w;
function Open()
{
w = window.open('data:,tra-ta-ta');
}
function Check(){
alert(w.document.domain);
}
</script>
</head>
<body>
<input type="button" onclick="Open();" value="Open Data Window" >
<input type="button" onclick="Check();" value="Check Domain" >
</body>
</html>
```
При нажатии кнопки Check Domain показывается домен начальной страницы:

### 1.3 changing origin
- [ ] Открыть страницу с одного поддомена (`a.hu.io`).
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Task 1.3</title>
<script>
var wnd;
function OpenWindow(){
wnd = window.open("https://b.hu.io/ui/task1.3_to_new_tab.html");
}
function CheckAccess(){
alert(wnd.document.title);
}
function ChangeDomain(){
document.domain = "hu.io";
}
</script>
</head>
<body>
<form>
<input type="button" value="Открыть страницу с другого источника" onclick="OpenWindow();"></input>
<br>
<input type="button" value="Проверить доступность другого окна" onClick="CheckAccess();"></input>
<br>
<input type="button" value="Сменить домен" onclick="ChangeDomain();"></input>
</form>
</body>
</html>
```

- [ ] С первой страницы открыть другую со второго домена (`b.hu.io`).

- [ ] Проверить взаимную доступность DOM через `Window` объект и `window.opener` соответственно.


- [ ] Провести смену домена (`document.domain = 'hu.io'`) и повторитть проверку.


- [ ] Открыть страницу с `Feature-Policy: document-domain 'none';` и попробовать сменить домен.
```
@collect_handler
def no_domain_setup(request: Request, response: Response):
value = request.query_params.get('NoDomainSetup', None)
if value is None:
return {}
else:
return {"Feature-Policy": "document-domain 'none';"}
```
Заголовок возымел действие только в Хроме:

### 1.4 interpage communication
- [ ] Сделать страницу, которая слушает сообщения и возвращает их обратно через `postMessage`.
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>1.4 Ping Pong Page</title>
<script>
window.addEventListener("message", (event) => {
event.source.postMessage(event.data, "*");
},
false);
</script>
</head>
<body>
</body>
</html>
```
- [ ] Обеспечить, чтобы обратное сообщение получила страница с тем же `Origin`.
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>1.4 Ping Pong Page Only to This Origin</title>
<script>
window.addEventListener("message", (event) => {
event.source.postMessage(event.data, "https://" + document.domain);
},
false);
</script>
</head>
<body>
</body>
</html>
```

### 1.5 Cross-Origin-Opener-Policy
- [ ] Сделать страницу, которая возвращает заголовок `Cross-Origin-Opener-Policy: same-origin`.
Заголовок:
```
@collect_handler
def coop_setup(request: Request, response: Response):
if str(request.url).endswith("task1.5.html"):
return {"Cross-Origin-Opener-Policy": "same-origin"}
else:
return {}
```
Сама страничка:
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Task 1.5</title>
</head>
<body>
<form>
<input type="button" value="Check Opener" onclick="alert(window.opener);"></input>
</form>
</body>
</html>
```
- [ ] Открыть эту страницу с другой страницы с **таким же** `Origin` и отличающимся.
Код открывающей страницы:
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Task 1.5 Opener</title>
<script>
var wnd;
function OpenFromSameOrigin(){
wnd = window.open("https://hu.io/ui/task1.5.html");
}
function OpenFromOtherOrigin(){
wnd = window.open("https://a.hu.io/ui/task1.5.html");
}
function CheckWnd(){
alert(wnd.document);
}
</script>
</head>
<body>
<form>
<input type="button" value="Open Page From the Same Origin" onclick="OpenFromSameOrigin();"></input>
<br>
<input type="button" value="Open Page From the Other Origin" onclick="OpenFromOtherOrigin();"></input>
<br>
<input type="button" value="Check Opened Document" onclick="CheckWnd();"></input>
</form>
</body>
</html>
```
- [ ] В обоих случаях посмотреть на `Window` объект и значение `window.opener`.
Различия в поведении в зависимости от origin мною не замечено:


Если честно, сколько ни пересматривал объяснение, сколько ни вчитывался в англоязычные обороты, так и не смог понять, зачем нужен этот заголовок. Вроде бы SOP и так должен запрещать доступ к документам из разных origin.
## 2. CORS
### 2.1 forms
- [ ] Сделать форму, которая отправляет POST запрос на другой `Origin`.
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Task 2.1</title>
</head>
<body>
<form action="https://a.hu.io/subscribe.asp">
Name:
<input type="text" name="name"></input>
<br>
E-mail:
<input type="email" name="email"></input>
<br>
<input type="submit" value="Subscribe">
</form>
</body>
</html>
```
- [ ] Посмотреть на предаваемые в запросе заголовки.

### 2.2 simple
- [ ] Выставить куки на домене `b.hu.io`.
```
@collect_handler
def set_cookie(request: Request, response: Response):
if str(request.url).endswith("hu.io/ui/task2.2_setCookie.html"):
return {"Set-Cookie": "task2.2_cookie=someValue; SameSite=None; Path=/"}
else:
if(str(request.url)).endswith("hu.io/ui/task2.3_setCookie.html"):
return {"Set-Cookie": "subscr_id=25; SameSite=None; Path=/"}
else:
return {}
```

- [ ] Реализовать эндпоинт-обработчик POST запроса, который возвращает:
- `Access-Control-Allow-Origin: <request_origin>`
Реализована выдача такого ответа в случае передачи query-параметра cors=1
```
@collect_handler
def set_cors(request: Request, response: Response):
value = request.query_params.get("cors", "0")
origin = request.headers.get("Origin", "")
if value == "1":
return {"Access-Control-Allow-Origin": origin}
else:
if value == "2":
return {"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": "true"}
else:
if value == "3":
return {"Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true", "Access-Control-Allow-Headers": "Content-Type"}
else:
return {}
```
- [ ] Сделать запрос с `a.hu.io` на этот эндпоинт.
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Task 2.2</title>
<script>
function DoRequest(cors_mode){
let r = new XMLHttpRequest();
r.open('POST', "https://b.hu.io/task2/subscribe?cors=" + cors_mode, false);
r.withCredentials = document.getElementById("wc").checked;
r.setRequestHeader('Content-Type', 'text/plain');
r.send("Subscribe me, please!!!");
alert(r.response);
}
</script>
</head>
<body>
<form>
<input type="button" value="Get Response With ACAO Header" onclick="DoRequest('1');"></input>
<br>
<input type="button" value="Get Response Without ACAO=* and ACAC Headers" onclick="DoRequest('2');"></input>
<br>
<input type="checkbox" name="wc" id="wc"> </input>
<label for="wc">withCredentials for the XMLHttpRequest</label>
</form>
</body>
</html>
```
- [ ] Посмотреть, были ли присланы куки.
Кука была отправлена, но браузер заблокировал ответ:

- [ ] Реализовать эндпоинт-обработчик POST запроса, который возвращает:
- `Access-Control-Allow-Origin: *`
- `Access-Control-Allow-Credentials: true`
Сделано в функции set_cors (см. выше). Такой ответ будет в случае передачи query-параметра cors=2
- [ ] Сделать запрос с `a.hu.io` на этот эндпоинт.
Кука были отправлена, но браузер снова заблокировал ответ:

- [ ] Описать ошибку из консоли.
Причина понятна: это противоречивые заголовки.
access-control-allow-credentials: true допустим только при указании конкретного домена в заголовке
access-control-allow-origin
### 2.3 preflight
- [ ] Реализовать эндпоинт, отвечающий на методы `OPTIONS` и `POST`. На `POST` **только с кукой**.
```
@app.post("/task2/data")
async def subscription_data(subscr_id: Optional[str] = Cookie(None)):
if subscr_id:
return {"subscription": True, "result": "Data were taken."}
else:
raise HTTPException(401, "Unauthorized")
@app.options("/task2/data")
async def subscription_data_preflight():
return ""
```
- [ ] Настроить хедеры в ответах так, чтобы запрос с другого `Origin` успешно отрабатывал и получал JSON-значение в ответ. (Разрешение кросс-сайт авторизованных запросов)
Это реализовано в ф-ции set_cors. Приведу её здесь вновь:
```
@collect_handler
def set_cors(request: Request, response: Response):
value = request.query_params.get("cors", "0")
origin = request.headers.get("Origin", "")
if value == "1":
return {"Access-Control-Allow-Origin": origin}
else:
if value == "2":
return {"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": "true"}
else:
if value == "3":
return {"Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true", "Access-Control-Allow-Headers": "Content-Type"}
else:
return {}
```
Сначала установка куки на b.hu.io:

Теперь собственно cors-запрос с a.hu.io:

## 3. CSP
### 3.1 default
- [ ] Реализовать страницу, на которую можно загружать ресурсы только с того же `Origin`, а картинки из любого места.
```
@collect_handler
def csp_only_self_except_images(request: Request, response: Response):
if str(request.url).endswith("task3.1.html"):
return {"Content-Security-Policy": "default-src 'self'; img-src *"}
else:
return {}
```
```
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Task 3.1</title>
</head>
<body>
<audio controls width="250">
<source src="https://b.hu.io/ui/07.mp3" type="audio/mpeg">
</audio>
<br>
<img src="https://b.hu.io/ui/IMG_3.JPG"/>
</body>
</html>
```
- [ ] Предъявить CSP.

### 3.2 unsafe-inline
- [ ] Реализовать страницу, на которую нельзя ничего загружать, но можно использовать inline стили и скрипты.
```
@collect_handler
def only_inline_scripts(request: Request, response: Response):
if str(request.url).endswith("task3.2.html"):
return {"Content-Security-Policy": "default-src 'none'; script-src 'unsafe-inline'"}
else:
return {}
```
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Task 3.2</title>
</head>
<body>
<form>
<input type="button" value="Check Script" onclick="alert('Inline scripts work');">
</form>
<br>
Image: <br>
<img src="IMG_3.JPG"/>
</body>
</html>
```
- [ ] Предъявить CSP.

### 3.3 connect-src
- [ ] Разрешить обращения через `Fetch API` только на **другой** `Origin`.
```
@collect_handler
def csp_connect_to_origin(request: Request, response: Response):
if str(request.url).endswith("task3.3.html"):
return {"Content-Security-Policy": "connect-src https://a.hu.io"}
else:
return {}
```
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Task 3.3</title>
<script>
function TryConnect(){
var x = new XMLHttpRequest();
x.open("GET", "https://a.hu.io/ui/task3.1.html?cors=3", false);
x.send();
alert(x.response);
}
function TryConnectB(){
var x = new XMLHttpRequest();
x.open("GET", "https://b.hu.io/ui/task3.1.html?cors=3", false);
x.send();
alert(x.response);
}
</script>
</head>
<body>
<form>
<input type="button" value="Try connect (A)" onclick="TryConnect();"></input>
<input type="button" value="Try connect (B)" onclick="TryConnectB();"></input>
</form>
</body>
</html>
```
- [ ] Предъявить CSP.

### 3.4 framing
- [ ] Сделать вложенную структуру из фреймов следующего вида:
1. Главная страница - origin='a.hu.io'.
2. Фрейм 1 на главной странице - origin='b.hu.io', защиты нет.
3. Фрейм 2 внутри фрейма 1 - origin='b.hu.io', защита `Same-Origin`.
```
@collect_handler
def csp_frame_from_this_domain(request: Request, response: Response):
if str(request.url).endswith("task3.4_frame1.html"):
return {"Content-Security-Policy": "frame-src 'self'"}
else:
return {}
```

- [ ] Для фрейма 2 разрешить `Same-Origin` фрейминг сначала с помощью `X-Frame-Options`, а потом с помощью `frame-ancestors`. Сравнить результаты.
C X-Frame-Options:
```
@collect_handler
def xfo_framing_from_this_domain(request: Request, response: Response):
if str(request.url).endswith("task3.4_frame2.html"):
return {"X-Frame-Options": "SAMEORIGIN"}
else:
return {}
```

C CSP:
```
@collect_handler
def csp_framing_from_this_domain(request: Request, response: Response):
if str(request.url).endswith("task3.4_frame2.html"):
return {"Content-Security-Policy": "frame-ancestors 'self'"}
else:
return {}
```

Таким образом, в Firefox разницы нет. Объяснение:

### 3.5 subresource integrity
- [ ] Вставить скрипт с CDN jquery с `integrity` и `crossorigin` аттрибутами. Проверить, что работает.

- [ ] Испортить хеш и посмотреть на ошибки.

- [ ] Убрать атрибуты `integrity` и `crossorigin` и разрешить эту библиотеку через CSP.
Не удалось побороть CSP. Даже подобрал ссылку, где хэш в base64-кодировке не содержит неоднозначных символов, но всё без толку, ошибку так и не смог найти. Буду признателен за подсказку, что не так
```
@collect_handler
def csp_check_script_hash(request: Request, response: Response):
if str(request.url).endswith("task3.5_csp.html"):
return {"Content-Security-Policy": "script-src 'sha256-HwWONEZrpuoh951cQD1ov2HUK5zA5DwJ1DNUXaM6FsY='"}
else:
return {}
```

## 4. Cookies
### 4.1 cookies identity
- [ ] Проверить пример из секции `Cookies identity`. Показать, какие куки прилетают.
```
@app.get("/task4/cart/change")
async def same_name_cookies(response: Response):
response.set_cookie("name", "A", path="/")
response.set_cookie("name", "B", path="/task4")
response.set_cookie("name", "C", path="/task4/cart")
response.set_cookie("name", "D", path="/task4/cart/change")
response.set_cookie("name", "E", path="/", domain=".hu.io")
response.set_cookie("name", "F", path="/task4", domain=".hu.io")
response.set_cookie("name", "G", path="/task4/cart", domain=".hu.io")
response.set_cookie("name", "H", path="/task4/cart/change", domain=".hu.io")
response.set_cookie("name", "I", path="/", domain="a.hu.io")
response.set_cookie("name", "J", path="/task4", domain="a.hu.io")
response.set_cookie("name", "K", path="/task4/cart", domain="a.hu.io")
response.set_cookie("name", "L", path="/task4/cart/change", domain="a.hu.io")
return {"message": "This page should set many cookies with the same name"}
```

### 4.2 same-site
- [ ] Выставить разные по значению куки на поддоменах `a.hu.io` и `b.hu.io`, на корень, с `SameSite=Strict`.
```
@app.get("/task4.2/setupCookie")
async def strict_cookies(response: Response):
response.set_cookie("coo42", "Root", path="/", domain="hu.io", samesite="strict")
response.set_cookie("coo42", "A", path="/", domain="a.hu.io", samesite="strict")
response.set_cookie("coo42", "B", path="/", domain="b.hu.io", samesite="strict")
return {"message": "This page should set many cookies with the same name (coo42)"}
```
Устанавливаю куки на a.hu.io

Устанавливаю куки на b.hu.io

- [ ] Сделать запросы на оба поддомена. Посмотреть какие куки прилетают.
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
Frame from A subdomain: <br>
<iframe src="https://a.hu.io/ui/frame1.html"></iframe>
Frame from B subdomain: <br>
<iframe src="https://b.hu.io/ui/frame1.html"></iframe>
</body>
</html>
```
Смотрим куки для запроса на поддомен A:

Смотрим куки для запроса на поддомен B:

С точки зрения атрибута куки SameSite эти запросы не являются кросс-ориджин (ориджином тут являются hu.io и все его поддомены), поэтому в этих запросах отправляются куки, включая куки с атрибутом SameSite=strict. При этом в каждом запросе отсылаются два значения куки coo42, т.к. значение куки, назначенное для домена hu.io, актуально и для его поддоменов.
### 4.3 history API
- [ ] Выставить разные куки на разные пути.
```
@app.get("/task4.3/setupCookie")
async def cookies_for_two_paths(response: Response):
response.set_cookie("cooA", "1", path="/ui/task4.3/pathA")
response.set_cookie("cooB", "2", path="/ui/task4.3/pathB")
return {"message": "This page should set two cookies"}
```

- [ ] Поменять путь документа с помощью History API.
Открываю эту страницу, которая лежит по пути PathA
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Task 4.3</title>
<script>
function OnButtonClick(){
window.history.pushState({"page": 1}, "My prev page", "https://hu.io/ui/task4.3/pathB/absent.html");
window.history.back();
let x = new XMLHttpRequest();
x.open("GET", "https://hu.io/ui/task4.3/pathB/empty.html", false);
x.send();
alert(x.response);
}
</script>
</head>
<body>
<form>
<input type="button" value="Change History and Make Request" onclick="OnButtonClick();">
</form>
</body>
</html>
```
При нажатии на кнопку меняется история на путь PathB и отправляется запрос на получение страницы, расположенной по пути PathB
- [ ] Отправить запрос на сервер. Посмотреть для какого пути прилетают куки.
Насколько я понимаю, поскольку я делаю запрос на путь PathB, вне зависимости от манипуляций с историей для такого запроса на сервер всё равно отправляется кука, записанная для пути PathB:

## 5. Other
### 5.1 mixed content
- [ ] Сделать страницу, которая включает как HTTPS ресурсы, так и HTTP.
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Task 5.1</title>
</head>
<body>
<img src="http://www.google.ru/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"/>
<br>
<img src="https://ost1.gismeteo.ru/assets/flat-ui/img/map-660.jpg"/>
</body>
</html>
```
- [ ] Посмотреть ошибки в консоли.

### 5.2 nosniff
- [ ] Сделать страницу, которая включает в качестве скрипта текстовый файл с расширением `.txt`.
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Task 5.2</title>
<script src="task5.2_script.txt"></script>
</head>
<body>
</body>
</html>
```
- [ ] Посмотреть, выполнится ли скрипт.
Да, скрипт выполнился:

В текстовом файле было:
```
alert("This message is shown from the text file");
```
- [ ] Добавить в ответ сервера исходной страницы `X-Content-Type-Options: nosniff`.
Оказалось, что данный заголовок должен выставляться в ответ на запрос файла скрипта, а не файла страницы
```
@collect_handler
def no_sniff(request: Request, response: Response):
if str(request.url).endswith("a.hu.io/ui/task5.2_script.txt"):
return {"X-Content-Type-Options": "nosniff"}
else:
return {}
```
- [ ] Посмотреть, выполнится ли скрипт. Посмотреть на ошибки в консоли.

### 5.3 referer
- [ ] Сделать `Same-Origin` и `Cross-Origin` запросы со страницы. Посмотреть в каком виде летит `Referer`.
- [ ] Запретить отправку `Referer` с этой страницы через `Referer-Policy`. Проверить на запросах из первого пункта.
### 5.4 spying service worker
- [ ] Установить шпионящего `Service Worker` на поддомен `a.hu.io`, который отправляет пути и параметры запросов на домен `b.hu.io`.