-
Notifications
You must be signed in to change notification settings - Fork 1
/
03.production.tex
132 lines (87 loc) · 17.3 KB
/
03.production.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
\chapter{Применение WebSocket в Web-среде}
Использование WebSocket на производственных серверах в условиях реального веба требует повышенного внимания к безопасности и производительности.
\section{Безопасность WebSocket}
Хотя WebSockets предлагают мощное средство для разработки приложений коммуникации в реальном времени, они также создают определенные угрозы безопасности, которые следует принимать во внимание. Вот некоторые распространенные проблемы безопасности, связанные с WebSockets, а также стратегии их устранения:
Перехват WebSocket между сайтами (CSWSH): возникает, когда злоумышленник использует уязвимость XSS для кражи соединения WebSocket и связи с сервером. Для предотвращения этого, разработчику важно убедиться, что соединение WebSocket устанавливается исключительно между доверенными сторонами. Внедрение аутентификации на стороне сервера поможет предотвратить такие атаки.
Подделка межсайтовых запросов (CSRF): возникает, когда злоумышленник отправляет запрос WebSocket на сервер от имени другого пользователя. Для предотвращения этого, важно использовать CSRF-токены, чтобы обеспечить прием запроса WebSocket только от доверенных источников.
Инъекционные атаки: они возникают, когда злоумышленник внедряет вредоносный код или данные в запрос или ответ WebSocket. Для предотвращения атак с использованием инъекций, важно скрупулезно проверять и фильтровать все данные, отправляемые через соединение WebSocket. Реализация проверки входящих данных и кодирования исходящих данных может помочь избежать инъекционных атак.
Атаки "отказ в обслуживании" (DoS): возникают, когда злоумышленник перегружает сервер большим числом запросов WebSocket, что может привести к сбою сервера. Для предотвращения DoS-атак важно внедрить ограничение скорости и дросселирование, чтобы снизить количество запросов, которые сервер способен обрабатывать.
Атаки "человек посередине" (MitM): возникают, когда злоумышленник перехватывает трафик WebSocket и просматривает или изменяет данные, передаваемые между клиентом и сервером. Чтобы предотвратить атаки MitM, важно внедрить меры шифрования и аутентификации, такие как SSL/TLS, чтобы обеспечить безопасность трафика WebSocket и защиту от перехвата или изменения.
В общем, важно надлежащим образом обезопасить соединение WebSocket, внедряя такие меры, как проверка вхождений, кодирование выходных данных, аутентификация, шифрование, ограничение скорости и CSRF-токены. Следуя рекомендациям по обеспечению безопасности WebSocket, разработчики смогут предотвратить типичные проблемы безопасности и обеспечить надежность и безопасность их приложений для обмена данными в реальном времени.
\subsection{Шифрование и обеспечение конфиденциальности}
Протокол WebSocket поддерживается практически всеми современными браузерами включая Chrome, Firefox, Internet Explorer, Opera, и Safari. Однако по спецификации, этот протокол не обеспечивает шифрования, а значит может быть перехвачен, прочитан и даже изменён. Поскольку WebSocket это централизованный протокол (т.е. предполагает наличие сервера) то проблему защиты данных нужно решать на стороне серверных платформ начинает поддерживать этот протокол.
NGINX с одной стороны умеет терминировать SSL/TLS траффик, с другой -- поддерживает WebSocket, позволяя установить туннель между клиентом и сервером бэкэнда\cite{label4}. Чтобы NGINX отправил запрос на обновление от клиента к серверу бэкэнда, заголовки Upgrade и Connection должны быть установлены явно, как в этом примере:
\begin{lstlisting}[style=CommandLineStyle]
location /wsapp/ {
proxy_pass http://wsbackend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
\end{lstlisting}
После этого NGINX обрабатывает это как соединение WebSocket.
\subsection{Атака ``отравленный кэш``}
В ранних реализациях WebSocket существовала уязвимость, известная как «cache poisoning» (отравленный кэш)\cite{label1}. Эта уязвимость позволяла атаковать кэширующие прокси-серверы, особенно корпоративные системы.
Атака выполнялась следующим образом:
\begin{enumerate}
\item Злоумышленник привлекает доверчивого пользователя (далее - Жертва) на свой сайт.
\item На сайте Злоумышленника, веб-страница открывает WebSocket-соединение из браузера Жертвы к серверу Злоумышленника. Предполагается, что жертва использует прокси-сервер, который является целью атаки.
\item Веб-страница создает особый WebSocket-запрос, который некоторые прокси-сервера не могут распознать (и в этом есть условие для успешной атаки).
\item Прокси-сервер Жертвы пропускают начальный запрос (содержащий Connection: upgrade) и полагает, что следующий HTTP-запрос уже поступил. Однако на самом деле вместо этого данные передаются через WebSocket! Обе стороны WebSocket (веб-страница и сервер) контролируются Злоумышленником!
\item Злоумышленник может передать что-то, похожее на GET-запрос к известному ресурсу, например: http://code.jquery.com/jquery.js. Тогда сервер ответит, имитируя код jQuery с кэширующими заголовками. Прокси-сервер получит этот ответ и закэширует искажённую версию jQuery.
\item В результате, при загрузке новых страниц, другие пользователи, использующие тот же прокси-сервер, что и Жертва, получат хакерский код вместо настоящего jQuery (это и объясняет название атаки - «отравленный кэш».).
\end{enumerate}
Эта атака не работает для всех прокси-серверов, однако при исследовании уязвимости было доказано, что реальные уязвимые прокси-серверы существуют.
Для предотвращения такой атаки разработали метод защиты - использование «маски».
\subsubsection{Маска для защиты от атаки}
Маска, которую мы рассматривали ранее при изучении полей WebSocket пакета, создана для предотвращения атаки отравленного кэша.
Ключ маски -- это случайное 32-битное значение, которое меняется для каждого пакета данных.
Тело сообщения проходит через \texttt{XOR} $\oplus$ с маской, а получатель восстанавливает данные, применяя повторно \texttt{XOR} с маской (можно легко доказать, что $(x \oplus a) \oplus a == x$).
Маска служит двум целям:
\begin{enumerate}
\item Генерируется браузером. Таким образом, злоумышленник больше не может контролировать реальное содержимое тела сообщения. После наложения маски данные превращаются в бинарный беспорядок.
\item Полученный пакет данных уже не может быть воспринят промежуточным прокси как HTTP-запрос.
\end{enumerate}
Наложение маски требует дополнительных ресурсов, поэтому в протоколе WebSocket оно не является обязательным.
Если используется два клиента (не обязательно браузеры), которые доверяют друг другу и посредникам, можно установить бит маски в 0, и тогда не потребуется указывать ключ маски.
\section{Производительность и масштабируемость}
Когда речь заходит о промышленном применении и высоких нагрузках, встаёт вопрос масштабируемости.
\subsubsection{Вертикальное и горизонтальное масштабирование}
Первый тип - вертикальное масштабирование, что является простым путем увеличения производительности приложения, хотя и имеет свои ограничения. Это, в перву очередь, ресурсы: берётся один экземпляр приложения и улучшаем аппаратное обеспечение (процессор, объем памяти, ввод-вывод и т.д.). Этот метод не требует дополнительных затрат с точки зрения разработки, однако не является самым эффективным для масштабирования.
Главным недостатком вертикального масштабирования является то, что время исполнения кода не изменяется линейно с улучшением оборудования. Кроме того, существует ограничение на возможности аппаратных усовершенствований - нет возможности неограниченно повышать производительность процессора.
Значит, есть другая альтернатива - горизонтальное масштабирование, которое предполагает создание дополнительных экземпляров приложения вместо добавления ресурсов к существующим. Это позволяет практически неограниченно масштабировать систему с возможностью динамического масштабирования при изменении нагрузок.
С точки зрения разработки, у нас появляется дополнительный элемент в системе -- балансировщик нагрузки. А в некоторых случаях, придётся внести изменения в архитектуру, добавив реализацию pub/sub.
\subsubsection{Проблема состояния и липкая сессия}
Простые REST API сервера не хранят состояние. И горизонтальное масштабирование для таких серверов превращается в тривиальную задачу: мы можем случайным образом раскидывать запросы клиентов, и нам не важно какой экземпляр приложения этот запрос обработает.
Вот так может выглядеть конфиг для балансировщика HAProxy \cite{label5}
\begin{lstlisting}[style=CommandLineStyle]
defaults
mode http
option http-server-close
timeout connect 5s
timeout client 30s
timeout client-fin 30s
timeout server 30s
timeout tunnel 1h
default-server inter 1s rise 2 fall 1 on-marked-down shutdown-sessions
option forwardfor
frontend all
bind 127.0.0.1:8080
default_backend backends
backend backends
server srv1 127.0.0.1:8081 check
server srv2 127.0.0.1:8082 check
\end{lstlisting}
с WebSocket ситуация иначе, так как каждое сокетное соединение привязано к определенному экземпляру. В таком случае, нужно гарантировать, что все запросы отдельных пользователей направляются к нужному бэкэнду.
Для решения этой проблемы можно использовать липкие сессии. Балансировщик нагрузки HAProxy вместо циклического перебора будет использовать стратегию с минимальным количеством подключений и добавим куки для привязки запросов пользователей к определенному бэкэнду. Тогда запрос от одного пользователя будет приходить всегда на один и тот же сервер.
\begin{lstlisting}[style=CommandLineStyle]
backend backends
balance leastconn
cookie serverid insert
server srv1 127.0.0.1:8081 check cookie srv1
server srv2 127.0.0.1:8082 check cookie srv2
\end{lstlisting}
\subsubsection{Рассылка широковещательных сообщений}
Предположим, что возникает необходимость отправить сообщение сразу всем пользователям системы. Либо пользователи подписаны на один канал, при этом подключены к разным физическим серверам. Очевидно, что решить эту задачу в рамках одного сервера невозможно, т.к. он просто не знает о всех пользователях.
Решение этой задачи заключается в организации связи между разными экземплярами приложения на разных серверах. Для этого все экземпляры могут быть подписаны на определенный канал и обрабатывать входящие сообщения. Это применение паттерна pub/sub о котором мы уже говорили ранее. Существует большое количество готовых продуктов подобного рода, таких как Redis, Kafka или Nats.