# 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. ![](https://i.imgur.com/QO1zSV7.png) - [ ] Открыть `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. ![](https://i.imgur.com/lC3hs88.png) Это запрещено 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> ``` ![](https://i.imgur.com/UDrPpgi.png) - [ ] С основной страницы открыть окно со схемой `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 показывается домен начальной страницы: ![](https://i.imgur.com/KF1shF8.png) ### 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> ``` ![](https://i.imgur.com/g3dEH6x.png) - [ ] С первой страницы открыть другую со второго домена (`b.hu.io`). ![](https://i.imgur.com/ixA3ZA4.png) - [ ] Проверить взаимную доступность DOM через `Window` объект и `window.opener` соответственно. ![](https://i.imgur.com/Cdxbead.png) ![](https://i.imgur.com/7zDXszv.png) - [ ] Провести смену домена (`document.domain = 'hu.io'`) и повторитть проверку. ![](https://i.imgur.com/zH14JDy.png) ![](https://i.imgur.com/kUcl8J1.png) - [ ] Открыть страницу с `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';"} ``` Заголовок возымел действие только в Хроме: ![](https://i.imgur.com/njaomWL.png) ### 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> ``` ![](https://i.imgur.com/svEYjnb.png) ### 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 мною не замечено: ![](https://i.imgur.com/su2fzm7.png) ![](https://i.imgur.com/gJgocZK.png) Если честно, сколько ни пересматривал объяснение, сколько ни вчитывался в англоязычные обороты, так и не смог понять, зачем нужен этот заголовок. Вроде бы 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> ``` - [ ] Посмотреть на предаваемые в запросе заголовки. ![](https://i.imgur.com/0gGcp3R.png) ### 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 {} ``` ![](https://i.imgur.com/n5aPCHD.png) - [ ] Реализовать эндпоинт-обработчик 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> ``` - [ ] Посмотреть, были ли присланы куки. Кука была отправлена, но браузер заблокировал ответ: ![](https://i.imgur.com/4dB61MY.png) - [ ] Реализовать эндпоинт-обработчик POST запроса, который возвращает: - `Access-Control-Allow-Origin: *` - `Access-Control-Allow-Credentials: true` Сделано в функции set_cors (см. выше). Такой ответ будет в случае передачи query-параметра cors=2 - [ ] Сделать запрос с `a.hu.io` на этот эндпоинт. Кука были отправлена, но браузер снова заблокировал ответ: ![](https://i.imgur.com/MKxtrX5.png) - [ ] Описать ошибку из консоли. Причина понятна: это противоречивые заголовки. 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: ![](https://i.imgur.com/H2uKPj1.png) Теперь собственно cors-запрос с a.hu.io: ![](https://i.imgur.com/a6Hpfhk.png) ## 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. ![](https://i.imgur.com/qu5MX3y.png) ### 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. ![](https://i.imgur.com/CiZYHUF.png) ### 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. ![](https://i.imgur.com/yuIgCsW.png) ### 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 {} ``` ![](https://i.imgur.com/K4XiHXb.png) - [ ] Для фрейма 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 {} ``` ![](https://i.imgur.com/00AfGcr.png) 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 {} ``` ![](https://i.imgur.com/LShUofp.png) Таким образом, в Firefox разницы нет. Объяснение: ![](https://i.imgur.com/RttVpXq.png) ### 3.5 subresource integrity - [ ] Вставить скрипт с CDN jquery с `integrity` и `crossorigin` аттрибутами. Проверить, что работает. ![](https://i.imgur.com/Uh40clG.png) - [ ] Испортить хеш и посмотреть на ошибки. ![](https://i.imgur.com/wy25tdG.png) - [ ] Убрать атрибуты `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 {} ``` ![](https://i.imgur.com/uO3VhK8.png) ## 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"} ``` ![](https://i.imgur.com/Hm6xQ8J.png) ### 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 ![](https://i.imgur.com/l4cB53S.png) Устанавливаю куки на b.hu.io ![](https://i.imgur.com/3e6e5HB.png) - [ ] Сделать запросы на оба поддомена. Посмотреть какие куки прилетают. ``` <!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: ![](https://i.imgur.com/ZtXtljO.png) Смотрим куки для запроса на поддомен B: ![](https://i.imgur.com/8EYO9ck.png) С точки зрения атрибута куки 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"} ``` ![](https://i.imgur.com/shbbGvA.png) - [ ] Поменять путь документа с помощью 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: ![](https://i.imgur.com/7qqXVZl.png) ## 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> ``` - [ ] Посмотреть ошибки в консоли. ![](https://i.imgur.com/ue2KHS0.png) ### 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> ``` - [ ] Посмотреть, выполнится ли скрипт. Да, скрипт выполнился: ![](https://i.imgur.com/EFXtEA6.png) В текстовом файле было: ``` 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 {} ``` - [ ] Посмотреть, выполнится ли скрипт. Посмотреть на ошибки в консоли. ![](https://i.imgur.com/R8vY2RX.png) ### 5.3 referer - [ ] Сделать `Same-Origin` и `Cross-Origin` запросы со страницы. Посмотреть в каком виде летит `Referer`. - [ ] Запретить отправку `Referer` с этой страницы через `Referer-Policy`. Проверить на запросах из первого пункта. ### 5.4 spying service worker - [ ] Установить шпионящего `Service Worker` на поддомен `a.hu.io`, который отправляет пути и параметры запросов на домен `b.hu.io`.