Category: Web

03
Апр
2021

🍏 Взаимодействие SwiftUI с вебом. Часть первая: WebView

Рутинная задача, без которой не обходится ни один проект – взаимодействие нативных приложений iOS со всемирной паутиной. В первой статье цикла разбираемся, как загружать веб-приложения в SwiftUI Views. СПОЙЛЕР: дальше будут рассмотрены боле…

31
Мар
2021

🎨 Создание дизайн-системы в Figma: практическое руководство

Сегодня мы разберемся с оптимизацией рабочего процесса дизайнера интерфейсов, позволяющей начинать работу над проектами гораздо быстрее. Создание собственной дизайн-системы – процесс долгий и трудоемкий, но со временем затраты окупаются.

26
Мар
2021

🕸 gRPC и все-все-все: Часть I. Введение

Для лучшего понимания gRPC стоит разобраться и с другими протоколами. В первой части цикла мы поговорим о REST, RPC и затронем одну из его реализаций – SOAP.

Все сущее дв…

22
Мар
2021

🕸 Что такое HTTP и HTTPS?

Рассказываем о популярных протоколах, на которых построена всемирная паутина, а также отвечаем на вопросы новичков. Без понимания основ перейти к изучению более сложных тем не получится, а значит самое время восполнить пробелы в знаниях.

16
Мар
2021

🎨 Руководство для начинающих по созданию и анимации изображений SVG

Векторная графика помогает сделать дизайн сайта уникальным и масштабируемым. На примере простого значка мы расскажем о процессе создания и анимации изображений в популярном формате SVG.

Использование SVG для стилизации веб-сайтов открывает целый мир новых возможностей. Это отличный способ сделать дизайн уникальным и удобным для пользователей. Одно из самых больших преимуществ SVG – его гибкость и возможность изменять внутренние элементы изображения при помощи CSS. Вы можете создавать многоцветные значки и иллюстрации, а также анимировать их с помощью JavaScript или все того же CSS. SVG – векторный формат. Он масштабируется без потери качества, а правильно оптимизированная картинка мало весит. На примере небольшой иконки мы рассмотрим, как нужно работать с такими изображениями.

Создание SVG

Самый простой способ создать SVG – написать код:

        <svg width="100" height="100">
  <circle cx="50" cy="50" r="40" fill="black" />
</svg>

    
Если вы хотите делать более сложные формы, лучше всего взять приложение для работы с векторной графикой, а затем экспортировать изображения в файлы SVG. Для этих целей, большинство разработчиков использует Adobe Illustrator или Sketch. Можно также отметить InkScape и онлайн-инструменты vectr.com или editor.method.ac.

Мы будем возьмем Illustrator. Как в любом приложении для работы с векторной графикой, большинство клиентских логотипов или иллюстраций представлены здесь в виде файлов AI, поэтому их можно легко открыть и экспортировать в SVG.

Для начала нужно создать новый веб-документ:


Размер холста – 400х400 пикселей. Размер на самом деле не имеет значения, потому что это вектор, но лучше оставить его в пределах, которые вы ожидаете увидеть в браузере. Будет легче поддерживать размер штриха и менять его, по сравнению с базовым. Далее создадим значок:


Важное практическое правило: чем меньше параметров, тем лучше! Так ваш код SVG будет чище и легче. Вы сделаете меньше ошибок, если не будете пытаться построить слишком сложные формы или рисованные иллюстрации.

При построении изображения можно использовать штрихи, различные цвета и даже градиенты. Все это будет экспортировано в SVG. Если вы планируете анимировать отдельные элементы значка, необходимо назвать слои и группы:


После экспорта имена будут преобразованы в идентификаторы: это будет полезно для более сложных иллюстраций. Лучше всего изменить идентификаторы на классы для внутренних элементов. Вы по-прежнему сможете использовать идентификатор для основного элемента <svg>, но им будет легче управлять, если пути и формы имеют классы.

Экспорт и оптимизация SVG-файлов

Когда значок будет готов, нужно экспортировать его в SVG. Для этого перейдите в: Файл -> Экспорт -> Экспортировать как.


Если вы хотите, чтобы холст SVG был того же же размера, что и монтажная область, или если вы экспортируете несколько значков из одного документа, установите флажок «Использовать монтажные области». В противном случае Illustrator обрежет холст до размера содержимого. Лучше всегда использовать размер артборда. Изображение будет легче поддерживать, если оно имеет определенный размер, особенно если нужно изменить или обновить иконку. Вы можете экспортировать его снова с тем же размером монтажной области.

Затем перед вами появится окно с параметрами для кода SVG:


В первом раскрывающемся списке необходимо выбрать способ создания CSS. Если у вас всего несколько элементов, лучше поставить «Inline style». Для более сложных иллюстраций есть «Internal CSS». Если вы не хотите использовать имена слоев и групп в качестве идентификаторов, измените раскрывающееся меню «Object IDs» на «Minimal». Это нужно для минимизации кода SVG перед отправкой в production. Если вы работаете над анимацией и хотите иметь более чистый код, снимите этот флажок.

После выбора опций экспортированный код значка выглядит примерно так:

        <svg id="my-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 290 290">
  <title>svg-icon</title>
  <circle id="circle" cx="145" cy="145" r="124.67" style="fill: none;stroke: #cfd8dc;stroke-miterlimit: 10;stroke-width: 20px"/>
  <polyline id="checkmark" points="88.75 148.26 124.09 183.6 201.37 106.32" style="fill: none;stroke: #21b587;stroke-linecap: round;stroke-linejoin: round;stroke-width: 25px"/>
</svg>

    

Теперь необходимо изменить идентификаторы внутренних элементов на классы и добавить некоторые атрибуты для лучшей доступности:

        <svg id="my-icon" aria-labelledby="svg-my-icon-title" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 290 290">
<title id="svg-my-icon-title">Checkmark Icon</title>
<circle class="circle" cx="145" cy="145" r="124.67" style="fill:none;stroke:#cfd8dc;stroke-miterlimit:10;stroke-width:20px"/>
<polyline class="checkmark" points="88.75 148.26 124.09 183.6 201.37 106.32" style="fill:none;stroke:#21b587;stroke-linecap:round;stroke-linejoin:round;stroke-width:25px"/>
</svg>

    

Файлы SVG не занимают много места, что может улучшить скорость загрузки страницы. Если вы хотите еще больше их оптимизировать, попробуйте отличный инструмент – SVGOMG.


Вы можете загрузить в него файл SVG и получить множество дополнительных опций его тонкой настройки. Будьте осторожны: если вы зайдете слишком далеко в настройке изображения, нарушите дизайн. Если у вас уже есть стили или анимация, убедитесь, что не потеряете после оптимизации классы и элементы.

Анимация и манипуляции с CSS

После добавления кода SVG на страницу, необходимо стилизовать его как и любой другой элемент HTML. Например так:

        #my-icon .circle {

  stroke: #21b587;

}
    

Вы без труда найдете множество библиотек JavaScript (Svg.js, SnapSVG) для создания сложных анимаций, однако в большинстве случаев добиться интересного эффекта можно, используя только свойства opacity, scale, translate и colors.

Далее создадим простую анимацию для круга:

         #my-icon .circle {
           	animation: circle-animation 0.5s ease-out forwards;
           	opacity: 0;
           	transform: scale(0.9);
           	transform-origin: center;
}
 
@keyframes circle-animation {
           	100% {
                           	opacity: 1;
                           	transform: scale(1);
           	}
}

    

Это простая масштабная анимация в сочетании с непрозрачностью. Если у вас есть несколько значков с похожим кругом, вы можете повторно использовать тот же код, что сэкономит много времени и усилий. Именно поэтому использование классов для отдельных элементов предпочтительнее уникальных идентификаторов.

Теперь посмотрим на анимацию значка галочки:

        #my-icon .checkmark {

            stroke-dasharray: 400;

            stroke-dashoffset: 400;

            stroke: #cfd8dc;

            transform-origin: center;

}

@keyframes checkmark-animation {

            40% {

                            transform: scale(1);

            }

            55% {

                            stroke: #cfd8dc;

                            transform: scale(1.2);

            }

            70% {

                            transform: scale(1);

            }

            100% {

                            stroke-dashoffset: 0;

                            transform: scale(1);

                            stroke: #21b587;

            }

}
   animation: checkmark-animation 1s ease-out forwards;
    

Дальше немного сложнее. Здесь мы используем stroke-dasharray и stroke-offset, чтобы создать эффект отрисовки значка. Подробнее этот метод описан в специальном руководстве. Также мы применяем масштабирование и меняем цвет обводки: как видите, код очень простой, но он дает довольно интересный эффект:

                    .icon-box {

                            padding: 100px;

                            position: relative;

                            width:200px;

            }

            .icon-box:before {

                            content: 'Hover to see animation.';

                            bottom: 5px;

                            display: block;

                            left: 0;

                            position: absolute;

                            text-align: center;

                            width: 100%;

            }

            .icon-box:hover .circle {

                            animation: circle-animation 0.5s ease-out forwards;

            }

            .icon-box:hover .checkmark {

                            animation: checkmark-animation 1s ease-out forwards;

                            animation-delay: 0.25s;

            }     

            #my-icon .circle {

                            opacity: 0;

                            transform: scale(0.9);

                            transform-origin: center;

            }

            #my-icon .checkmark {

                            stroke-dasharray: 400;

                            stroke-dashoffset: 400;

                            transform-origin: center;

                            stroke: #cfd8dc;

            }

            @keyframes circle-animation {

                            100% {

                                           opacity: 1;

                                           transform: scale(1);

                            }

            }

            @keyframes checkmark-animation {

                            40% {

                                           transform: scale(1);

                            }

                            55% {

                                           stroke: #cfd8dc;

                                           transform: scale(1.2);

                            }

                            70% {

                                           transform: scale(1);

                            }

                            100% {

                                           stroke-dashoffset: 0;

                                           transform: scale(1);

                                           stroke: #21b587;

                            }

            }
                        border: 1px solid #eee;
    
        <div class="icon-box">                                   	
<svg id="my-icon" aria-labelledby="svg-my-icon-title" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 290 290">
<title id="svg-my-icon-title">My Icon</title>
<circle class="circle" cx="145" cy="145" r="124.67" style="fill:none;stroke:#cfd8dc;stroke-miterlimit:10;stroke-width:20px"/>
<polyline class="checkmark" points="88.75 148.26 124.09 183.6 201.37 106.32" style="fill:none;stroke:#21b587;stroke-linecap:round;stroke-linejoin:round;stroke-width:25px"/>
</svg>
</div>

    

Результаты наших экспериментов можно увидеть здесь.

***

Мы привели очень простой пример, однако он показывает потенциал SVG для веб-проектов. Есть много разных способов использования популярного формата изображений и векторной анимации. Также существует неплохая спецификация этого, по сути, языка разметки XML и несколько справочников SVG-элементов и SVG-интерфейсов, с которыми читателям будет полезно ознакомиться.

Удачи вам в обучении и в создании красивых сайтов!

13
Мар
2021

🕵 Примеры атак XSS и способов их устранения

Понимание межсайтового скриптинга и способов борьбы с ним необходимо каждому веб-разработчику. Это один и самых распространенных видов уязвимостей – злоумышленники часто проводят атаки XSS для кражи данных и нарушения работоспособности сервисов.

Перевод публикуется с сокращениями, автор оригинальной статьи Russel Jones.

Межсайтовый
скриптинг (
XSS)
– это атака, которая позволяет
JavaScript через
один сайт работать с другим.
XSS интересен не из-за технической
сложности, а скорее потому, что он эксплуатирует некоторые из основных
механизмов безопасности браузеров и из-за огромной распространенности.

Background

Изначально Всемирная Паутина представляла собой набор статических документов HTML, которые браузер должен был отображать для просмотра пользователями. По мере развития Интернета возрастали и требования к документам, что привело к появлению JavaScript и файлов cookie: скрипты нужны для интерактивности документа, а cookies – чтобы браузеры могли сохранять его состояние.

Появление этих возможностей привело к тому, что браузеры не только визуализируют HTML,
но и вмещают в памяти в качестве API для разработчиков представление,
называемое объектной моделью документа (DOM). DOM предлагает древовидную структуру тегов HTML, а также доступ к файлам cookie для
получения состояния. Со временем модель превратилась из предназначенной преимущественно для чтения структуры в структуру read-write, обновление которой приводит к повторному рендерингу документа.

Как только документы
получили возможность запускать код, браузеры должны были определить контекст
выполнения для программ на JavaScript. Политика, которая была разработана, называется
Same-Origin и по-прежнему является одним из фундаментальных примитивов безопасности
браузера. Изначально в ней утверждалось, что JavaScript в одном документе может
получить доступ только к собственному DOM и к DOM других документов с тем же
происхождением. Позже, когда был добавлен XMLHttpRequest
и Fetch, появилась модифицированная версия
Same-Origin. Эти API не могут
выдавать запросы к любому источнику, они могут только читать ответ на запросы
от того же источника.

Что же такое происхождение? Это кортеж протокола, имени хоста и порта документа.

Фрагмент 1: Кортеж из схемы, хоста и порта этого URL-адреса.
        https://www.example.com:443/app
^^^^^   ^^^^^^^^^^^^^^^ ^^^
Scheme  Host            Port
    
Рис. 1: Иллюстрация Same-Origin в действии. JavaScript работает на www.evil.com и не может получить доступ к DOM на www.example.com.
Рис. 1: Иллюстрация Same-Origin в действии. JavaScript работает на www.evil.com и не может получить доступ к DOM на www.example.com.

Политика Same-Origin
отлично помогает смягчать атаки на статические сайты, как показано на рисунке выше.
Однако с атаками на динамические ресурсы, принимающие пользовательский ввод, ситуация
немного сложнее из-за смешивания кода и данных, которая позволяет злоумышленнику
выполнять контролируемый ввод в исходном документе.

Атаки XSS обычно бывают трех видов: рефлективными, хранимыми и основанными на DOM.

Рефлективные и хранимые
XSS-атаки принципиально одинаковы, поскольку полагаются на вредоносный ввод,
отправляемый на бекенд и представляющий этот ввод пользователю сервер.
Рефлективные XSS обычно возникают в виде злонамеренно созданной злоумышленником
ссылки, по которой затем переходит жертва. Хранимые XSS происходят, когда
злоумышленник загружает вредоносный ввод. Атаки на основе DOM отличаются тем,
что они происходят исключительно на стороне клиента и включают вредоносный ввод, манипулирующий DOM.

Примеры

Рефлективные атаки XSS

Ниже можно увидеть
простое веб-приложение на Go, которое отражает свой ввод (даже если
это вредоносный скрипт) обратно пользователю. Вы можете использовать это
приложение, сохранив его в файле xss1.go и запустив go run xss1.go.

Фрагмент 3: Пример веб-приложения с рефлективной (отраженной) XSS-атакой.
        package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("X-XSS-Protection", "0")

    messages, ok := r.URL.Query()["message"]
    if !ok {
       messages = []string{"hello, world"}
    }
    fmt.Fprintf(w, "<html><p>%v</p></html>", messages[0])
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Чтобы увидеть
XSS-атаку, перейдите по уязвимому URL-адресу ниже.

        http://localhost:8080?message=<script>alert(1)</script>
    

Взгляните на источник: сервер вернул документ, который выглядит примерно так, как показано во фрагменте
4. Обратите внимание, как смешение кода и данных позволило произойти этой
атаке.

Фрагмент 4: Пример вывода уязвимого для XSS веб-приложения.
        <html>
  <p>
    <script>alert(1)</script>
  </p>
</html>
    

Этот пример может
показаться неправдоподобным, поскольку защита XSS была явно отключена. Эта ее форма основана на эвристике с обходными путями для
различных браузеров. Она была отключена для создания кроссбраузерных примеров,
иллюстрирующих основные концепции XSS-атак. Некоторые браузеры
удаляют эту защиту: например, в
Google Chrome 78
и выше вам не понадобится строка
w.Header().Set(“X-XSS-Protection”, “0”), чтобы атака сработала.

Хранимые XSS-атаки

Хранимые XSS-атаки похожи на рефлективные, но пэйлоад поступает из хранилища данных, а не из ввода непосредственно. Например, злоумышленник может загрузить в веб-приложение зловреда, который затем будет показан каждому авторизованному юзеру.

Ниже приведен простой чат, который иллюстрирует этот вид атак. Вы можете сохранить приложение в файле xss2.go и
запустить с помощью команды go run xss2.go.

Фрагмент 5: Хранимая XSS-атака.
        package main

import (
	"fmt"
	"log"
	"net/http"
	"strings"
	"sync"
)

var db []string
var mu sync.Mutex

var tmpl = `
<form action="/save">
  Message: <input name="message" type="text"><br>
  <input type="submit" value="Submit">
</form>
%v
`

func saveHandler(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	defer mu.Unlock()

	r.ParseForm()
	messages, ok := r.Form["message"]
	if !ok {
		http.Error(w, "missing message", 500)
	}

	db = append(db, messages[0])

	http.Redirect(w, r, "/", 301)
}

func viewHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")

	var sb strings.Builder
	sb.WriteString("<ul>")
	for _, message := range db {
		sb.WriteString("<li>" + message + "</li>")
	}
	sb.WriteString("</ul>")

	fmt.Fprintf(w, tmpl, sb.String())
}

func main() {
	http.HandleFunc("/", viewHandler)
	http.HandleFunc("/save", saveHandler)
	log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Чтобы увидеть атаку
XSS, перейдите по ссылке http://localhost:8080 и введите сообщение <script>alert(1);</script>.

Атака делится на две
фазы:

  • пейлоад сохраняется в хранилище данных в функции storeHandler;
  • когда страница визуализируется во ViewHandler, пейлоад добавляется к выходным данным.

XSS-атаки на основе DOM

Такие атаки не связаны с бекендом и происходят исключительно на стороне клиента. Они интересны тем, что современные веб-приложения перемещают логику к клиенту, а атаки происходят, когда пользователь напрямую манипулирует DOM. Хорошей новостью для злоумышленников является то, что DOM имеет широкий спектр способов эксплуатации, наиболее популярными из которых являются innerHTML и document.write.

Ниже приведен пример обслуживающего статический контент веб-приложения. Код тот же, что и в
примере с рефлективными XSS, но здесь атака будет происходить полностью на
стороне клиента. Вы можете сохранить приложение в файле xss3.go и запустить его командой go run xss3.go.

Фрагмент 6: Пример веб-приложения с XSS-атакой на основе DOM.
        package main

import (
    "fmt"
    "log"
    "net/http"
)

const content = `

<html>
   <head>
       <script>
          window.onload = function() {
             var params = new URLSearchParams(window.location.search);
             p = document.getElementById("content")
             p.innerHTML = params.get("message")
	     };
       </script>
   </head>
   <body>
       <p id="content"></p>
   </body>
</html>
`

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("X-XSS-Protection", "0")
    fmt.Fprintf(w, content)
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Чтобы увидеть эту
атаку, перейдите по ссылке
http://localhost:8080/?message=”<img src=1 onerror=alert(1);/>”. Обратите внимание, что вектор атаки немного
отличается и innerHTML не будет
выполнять скрипт напрямую, однако он добавит HTML-элементы, которые затем выполнят код на JavaScript. В приведенном примере добавляется элемент image, который запускает скрипт при возникновении ошибки (она всегда появляется, поскольку злоумышленник подставляет неверный источник).

Если хотите напрямую добавить
элемент скрипта, придется использовать другой приемник XSS. Замените
элемент script из фрагмента 6 элементом script из фрагмента 7 и перейдите
по следующей ссылке:
http://localhost:8080/?message=”<script>alert(1);</script>”. Атака сработает, потому что document.write принимает элементы
скрипта напрямую.

Фрагмент 7: Еще один пример атаки XSS на основе DOM.
        <script>
   window.onload = function() {
      var params = new URLSearchParams(window.location.search);
      document.open();
      document.write(params.get("message"));
      document.close();
   };
</script>
    

Связанные направления атак

Хотя обычно их не называют атаками XSS, существует несколько связанных направлений, о которых стоит упомянуть.

Content-type

Всему виной
неправильная настройка типа содержимого ответов HTTP. Это может произойти как
на уровне бекенда (ответ имеет неверный набор заголовков Content-Type), так и при попытке браузера проснифферить тип MIME.
Internet Explorer был особенно восприимчив к этому, и классическим примером
является служба загрузки изображений: злоумышленник может загрузить JavaScript вместо картинки.
Браузер видит, что тип контента был установлен на image/jpg, но пейлоад содержит скрипт – он выполняется, что приводит к атаке XSS.

Urlschemes

Следующий тип атаки –
активность через URL со схемой JavaScript. Представим веб-сайт, который
позволяет пользователю контролировать цель ссылки, как показано во фрагменте 8. В этом случае злоумышленник сможет предоставить
URL, выполняющий некий JavaScript с помощью нашей схемы.

Чтобы опробовать этот
тип атаки, можно сохранить приложение в файле xss4.go, запустить командой go run xss4.go и перейти по ссылке http://localhost:8080?link=javascript:alert(1).

Фрагмент 8: XSS-атака, введенная через схему URL-адресов.
        package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("X-XSS-Protection", "0")

    links, ok := r.URL.Query()["link"]
    if !ok {
        messages = []string{"example.com"}
    }
    fmt.Fprintf(w, `<html><p><a href="%v">Next</p></html>`, links[0])
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Избавление

Единого метода
решения данной проблемы не существует, иначе XSS не был бы
такой распространенной проблемой. Фундаментальная сложность вызвана отсутствием разделения между кодом и данными. Смягчение последствий XSS
обычно включает очистку входных данных (нужно убедиться, что они не
содержат кода), экранирование выходных данных (они также не должны содержать код) и реструктуризацию приложения таким образом, чтобы код
загружался из строго определенных конечных точек.

Валидация входных данных

Первая линия защиты – проверка входных данных. Убедитесь, что их формат соответствует ожидаемым характеристикам – эдакий белый список, гарантирующий отсутствие у приложения возможности принимать код.

Валидация
данных – сложная проблема. Не существует универсального инструмента или техники
для всех ситуаций. Лучше всего структурировать приложение таким образом, чтобы
оно требовало от разработчиков продумать тип принимаемых данных и обеспечить
удобное место, где можно разместить валидатор.

Хороший тон написания
приложений на Go состоит в том, чтобы не иметь никакой логики приложения в
обработчиках запросов HTTP, а вместо этого использовать их для анализа и проверки входных данных. Затем данные отправляются
в
обрабатывающую логику структуру. Обработчики запросов становятся
простыми и обеспечивают удобное централизованное расположение для контроля
правильности очистки данных.

На фрагменте 9
показано, как можно переписать saveHandler для приема символов ASCII [A-Za-z\.].

Фрагмент 9: Пример использования обработчиков HTTP-запросов для проверки данных.
        func saveHandler(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	messages, ok := r.Form["message"]
	if !ok {
		http.Error(w, "missing message", 500)
	}

	re := regexp.MustCompile(`^[A-Za-z\\.]+$`)
	if re.Find([]byte(messages[0]))) == "" {
		http.Error(w, "invalid message", 500)
	}
  
	db.Append(messages[0])

	http.Redirect(w, r, "/", 301)
}
    

Может показаться, что
это излишнее беспокойство, но чат-приложение принимает гораздо больше, чем ограниченный набор символов. Многие принимаемые приложениями данные достаточно структурированы: адреса, номера телефонов,
почтовые индексы и тому подобные вещи могут и должны быть
проверены.

Экранирование

Следующий шаг – экранирование вывода. В случае с нашим чатом все извлеченное из базы данных включалось непосредственно в выходной документ.

Одно и то же приложение
может быть гораздо безопаснее (даже если в него была произведена инъекция кода),
если экранировать все небезопасные выходные данные. Именно это делает пакет
html/template в Go. Использование языка шаблонов и
контекстно-зависимого синтаксического анализатора для экранирования данных до
их визуализации уменьшит вероятность выполнения вредоносного кода.

Ниже приведен пример
использования пакета html/template. Сохраните приложение в файле xss5.go, а затем выполните командой go run xss5.go.

Фрагмент 10: Использование экранирования для устранения хранимых XSS-атак.
        package main

import (
	"bytes"
	"html/template"
	"io"
	"log"
	"net/http"
	"sync"
)

var db []string
var mu sync.Mutex

var tmpl = `
<form action="/save">
  Message: <input name="message" type="text"><br>
  <input type="submit" value="Submit">
</form>
<ul>
{{range .}}
    <li>{{.}}</li>
{{end}}
</ul>`

func saveHandler(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	defer mu.Unlock()

	r.ParseForm()
	messages, ok := r.Form["message"]
	if !ok {
		http.Error(w, "missing message", 500)
	}

	db = append(db, messages[0])

	http.Redirect(w, r, "/", 301)
}

func viewHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")

	t := template.New("view")

	t, err := t.Parse(tmpl)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	var buf bytes.Buffer

	err = t.Execute(&buf, db)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	io.Copy(w, &buf)
}

func main() {
	http.HandleFunc("/", viewHandler)
	http.HandleFunc("/save", saveHandler)
	log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Опробуйте использованную ранее атаку XSS, перейдя по ссылке http://localhost:8080 и введите <script>alert(1);</script>.
Обратите внимание, что предупреждение не было вызвано.

Откройте консоль браузера и посмотрите на элемент li в DOM. Интерес представляют два
свойства: innerHTML и innerText.

Фрагмент 11: Проверка DOM при использовании экранирования.
        innerHTML: "<script>alert(1);</script>"
innerText: "<script>alert(1);</script>"
    

Обратите внимание, как
с помощью экранирования удалось четко разделить код и данные.

Content Security Policy

Content Security Policy (CSP) позволяет веб-приложениям определять набор доверенных источников для загрузки контента (например, скриптов). CSP можно использовать для разделения кода и данных, отказываясь от встроенных скриптов и загружая их только из определенных источников.

Написание CSP для
небольших автономных приложений является простой задачей – начните с политики,
которая по умолчанию запрещает все источники, а затем разрешите небольшой их набор.
Однако написать эффективный CSP для больших сайтов уже не так просто. Как
только сайт начинает загружать контент из внешних источников, CSP раздувается и
становится громоздким. Некоторые разработчики сдаются и включают директиву unsafe-inline, полностью разрушая теорию
CSP.

Чтобы упростить
написание CSP, в CSP3 вводится директива strict-dynamic.
Вместо того чтобы поддерживать большой белый список надежных источников,
приложение генерирует случайное число (nonce) каждый раз, когда запрашивается
страница. Этот nonce отправляется вместе с заголовками страницы и встроен в тег
script, что заставляет браузеры доверять этим скриптам с соответствующим nonce,
а также любым скриптам, которые они могут загрузить. Вместо
того, чтобы вносить скрипты в белый список и пытаться выяснить, какие еще сценарии они загружают, а затем пополнять белый список рекурсивно,
вам нужно достаточно внести в белый список импортируемый скрипт верхнего уровня.

Используя предложенный Google подход Strict CSP, рассмотрим простое приложение, принимающее
пользовательский ввод. Сохраните его в файле xss6.go, а затем выполните командой go run xss6.go.

Фрагмент 12: Пример CSP, смягчающего XSS-атаку.
        package main

import (
	"bytes"
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"strings"
)

const scriptContent = `
document.addEventListener('DOMContentLoaded', function () {
   var updateButton = document.getElementById("textUpdate");
   updateButton.addEventListener("click", function() {
      var p = document.getElementById("content");
      var message = document.getElementById("textInput").value;
      p.innerHTML = message;
   });
};
`

const htmlContent = `
<html>
   <head>
      <script src="script.js" nonce="{{ . }}"></script>
   </head>
   <body>
       <p id="content"></p>

       <div class="input-group mb-3">
         <input type="text" class="form-control" id="textInput">
         <div class="input-group-append">
           <button class="btn btn-outline-secondary" type="button" id="textUpdate">Update</button>
         </div>
       </div>

       <blockquote class="twitter-tweet" data-lang="en">
         <a href="https://twitter.com/jack/status/20?ref_src=twsrc%5Etfw">March 21, 2006</a>
       </blockquote>
       <script async src="https://platform.twitter.com/widgets.js"
         nonce="{{ . }}" charset="utf-8"></script>
   </body>
</html>
`

func generateNonce() (string, error) {
	buf := make([]byte, 16)
	_, err := rand.Read(buf)
	if err != nil {
		return "", err
	}

	return base64.StdEncoding.EncodeToString(buf), nil
}

func generateHTML(nonce string) (string, error) {
	var buf bytes.Buffer

	t, err := template.New("htmlContent").Parse(htmlContent)
	if err != nil {
		return "", err
	}

	err = t.Execute(&buf, nonce)
	if err != nil {
		return "", err
	}

	return buf.String(), nil
}

func generatePolicy(nonce string) string {
	s := fmt.Sprintf(`'nonce-%v`, nonce) 
	var contentSecurityPolicy = []string{
		`object-src 'none';`,
		fmt.Sprintf(`script-src %v 'strict-dynamic';`, s),
		`base-uri 'none';`,
	}
	return strings.Join(contentSecurityPolicy, " ")
}

func scriptHandler(w http.ResponseWriter, r *http.Request) {
	nonce, err := generateNonce()
	if err != nil {
		returnError()
		return
	}

	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
	w.Header().Set("Content-Security-Policy", generatePolicy(nonce))

	fmt.Fprintf(w, scriptContent)
}

func htmlHandler(w http.ResponseWriter, r *http.Request) {
	nonce, err := generateNonce()
	if err != nil {
		returnError()
		return
	}

	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Header().Set("Content-Security-Policy", generatePolicy(nonce))

	htmlContent, err := generateHTML(nonce)
	if err != nil {
returnError()
		return
	}

	fmt.Fprintf(w, htmlContent)
}

func returnError() {
http.Error(w, http.StatusText(http.StatusInternalServerError),
		http.StatusInternalServerError)
}

func main() {
	http.HandleFunc("/script.js", scriptHandler)
	http.HandleFunc("/", htmlHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}
    

Чтобы попытаться
использовать приложение, перейдите по ссылке: http://localhost:8080 и
попробуйте отправить <img src=1 onerror”alert(1)”/> как и раньше. Эта атака сработала бы и без CSP, но
поскольку CSP не допускает inline-скриптов, вы должны увидеть примерно такой вывод в консоли браузера:

«Отказано в выполнении встроенного обработчика событий, поскольку он нарушает следующую директиву CSP: “script-src ‘nonce-XauzABRw9QtE0bzoiRmslQ==’ ‘unsafe-inline’ ‘unsafe-eval’ ‘strict-dynamic’ https: http:” Обратите внимание, что ‘unsafe-inline‘ игнорируется, если в исходном списке присутствует либо хэш, либо значение nonce.»

Почему сценарий не
запустился? Рассмотрим
CSP подробнее.

Фрагмент 13: Базовый CSP. Nonce повторно генерируется для каждого запроса.
        script-src 'strict-dynamic' 'nonce-XauzABRw9QtE0bzoiRmslQ==';
object-src 'none';
base-uri 'none';
    
Что делает эта политика? Директива script-src включает strict-dynamic и значение nonce, используемое для загрузки скриптов. Это означает, что единственные скрипты, которые будут загружены, находятся в script elements, где nonce включен в атрибут, а значит inline-скрипты не загрузятся. Последние две директивы препятствуют загрузке плагинов и изменению базового URL приложения.

Основная сложность
использования этого подхода заключается в необходимости генерировать nonce и инжектить его в заголовки при каждой загрузке страницы. После этого шаблон
может быть применен ко всем загружаемым страницам.

Соответствующие методы устранения

Content-Type

Вы
должны не только устанавливать свой Content-Type, но и следить, чтобы браузеры не
пытались автоматически определить тип контента. Для
этого используйте заголовок: X-Content-Type-Options:
nosniff
.

Virtual doms

Хотя виртуальные домены
не являются функцией безопасности, использующие их современные фреймворки (
React и Vue) могут помочь смягчить атаки XSS на основе DOM.

Эти фреймворки создают DOM параллельно с тем, который находится в браузере, и сравнивают их. Отличающуюся часть DOM браузера они обновляют. Для этого необходимо создать виртуальный DOM, что приведет к уменьшению использования клиентами innerHTML и подтолкнет разработчиков к переходу на innerText.

React требует
использования атрибута
dangerouslySetInnerHTML, в то время как создатели Vue предупреждают, что использование
innerHTML может привести к появлению уязвимостей.

Заключение

Если вы дочитали до
конца, у вас может появиться желание разобраться, как работают браузеры, что такое ошибки
XSS и насколько важно понимать, как от них избавиться.
XSS трудно искоренить, поскольку приложения становятся все больше и все сложнее. Применяя упомянутые в статье методы, можно сделать жизнь злоумышленников трудной.

Удачи в борьбе и учебе!

Дополнительные материалы:

13
Мар
2021

🕵 Примеры атак XSS и способов их ослабления

Понимание межсайтового скриптинга и способов борьбы с ним необходимо каждому веб-разработчику. Это один из самых распространенных видов уязвимостей – злоумышленники часто проводят атаки XSS для кражи данных и нарушения работоспособности сервисов.

Перевод публикуется с сокращениями, автор оригинальной статьи Russel Jones.

Межсайтовый
скриптинг (
XSS)
– это атака, которая позволяет
JavaScript через
один сайт работать с другим.
XSS интересен не из-за технической
сложности, а скорее потому, что он эксплуатирует некоторые из основных
механизмов безопасности браузеров и из-за огромной распространенности.

Background

Изначально Всемирная Паутина представляла собой набор статических документов HTML, которые браузер должен был отображать для просмотра пользователями. По мере развития Интернета возрастали и требования к документам, что привело к появлению JavaScript и файлов cookie: скрипты нужны для интерактивности документа, а cookies – чтобы браузеры могли сохранять его состояние.

Появление этих возможностей привело к тому, что браузеры не только визуализируют HTML,
но и вмещают в памяти в качестве API для разработчиков представление,
называемое объектной моделью документа (DOM). DOM предлагает древовидную структуру тегов HTML, а также доступ к файлам cookie для
получения состояния. Со временем модель превратилась из предназначенной преимущественно для чтения структуры в структуру read-write, обновление которой приводит к повторному рендерингу документа.

Как только документы
получили возможность запускать код, браузеры должны были определить контекст
выполнения для программ на JavaScript. Политика, которая была разработана, называется
Same-Origin и по-прежнему является одним из фундаментальных примитивов безопасности
браузера. Изначально в ней утверждалось, что JavaScript в одном документе может
получить доступ только к собственному DOM и к DOM других документов с тем же
происхождением. Позже, когда был добавлен XMLHttpRequest
и Fetch, появилась модифицированная версия
Same-Origin. Эти API не могут
выдавать запросы к любому источнику, они могут только читать ответ на запросы
от того же источника.

Что же такое происхождение? Это кортеж протокола, имени хоста и порта документа.

Фрагмент 1: Кортеж из схемы, хоста и порта этого URL-адреса.
        https://www.example.com:443/app
^^^^^   ^^^^^^^^^^^^^^^ ^^^
Scheme  Host            Port
    
Рис. 1: Иллюстрация Same-Origin в действии. JavaScript работает на www.evil.com и не может получить доступ к DOM на www.example.com.
Рис. 1: Иллюстрация Same-Origin в действии. JavaScript работает на www.evil.com и не может получить доступ к DOM на www.example.com.

Политика Same-Origin
отлично помогает смягчать атаки на статические сайты, как показано на рисунке выше.
Однако с атаками на динамические ресурсы, принимающие пользовательский ввод, ситуация
немного сложнее из-за смешивания кода и данных, которая позволяет злоумышленнику
выполнять контролируемый ввод в исходном документе.

Атаки XSS обычно бывают трех видов: рефлективными, хранимыми и основанными на DOM.

Рефлективные и хранимые
XSS-атаки принципиально одинаковы, поскольку полагаются на вредоносный ввод,
отправляемый на бекенд и представляющий этот ввод пользователю сервер.
Рефлективные XSS обычно возникают в виде злонамеренно созданной злоумышленником
ссылки, по которой затем переходит жертва. Хранимые XSS происходят, когда
злоумышленник загружает вредоносный ввод. Атаки на основе DOM отличаются тем,
что они происходят исключительно на стороне клиента и включают вредоносный ввод, манипулирующий DOM.

Примеры

Рефлективные атаки XSS

Ниже можно увидеть
простое веб-приложение на Go, которое отражает свой ввод (даже если
это вредоносный скрипт) обратно пользователю. Вы можете использовать это
приложение, сохранив его в файле xss1.go и запустив go run xss1.go.

Фрагмент 3: Пример веб-приложения с рефлективной (отраженной) XSS-атакой.
        package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("X-XSS-Protection", "0")

    messages, ok := r.URL.Query()["message"]
    if !ok {
       messages = []string{"hello, world"}
    }
    fmt.Fprintf(w, "<html><p>%v</p></html>", messages[0])
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Чтобы увидеть
XSS-атаку, перейдите по уязвимому URL-адресу ниже.

        http://localhost:8080?message=<script>alert(1)</script>
    

Взгляните на источник: сервер вернул документ, который выглядит примерно так, как показано во фрагменте
4. Обратите внимание, как смешение кода и данных позволило произойти этой
атаке.

Фрагмент 4: Пример вывода уязвимого для XSS веб-приложения.
        <html>
  <p>
    <script>alert(1)</script>
  </p>
</html>
    

Этот пример может
показаться неправдоподобным, поскольку защита XSS была явно отключена. Эта ее форма основана на эвристике с обходными путями для
различных браузеров. Она была отключена для создания кроссбраузерных примеров,
иллюстрирующих основные концепции XSS-атак. Некоторые браузеры
удаляют эту защиту: например, в
Google Chrome 78
и выше вам не понадобится строка
w.Header().Set(“X-XSS-Protection”, “0”), чтобы атака сработала.

Хранимые XSS-атаки

Хранимые XSS-атаки похожи на рефлективные, но пэйлоад поступает из хранилища данных, а не из ввода непосредственно. Например, злоумышленник может загрузить в веб-приложение зловреда, который затем будет показан каждому авторизованному юзеру.

Ниже приведен простой чат, который иллюстрирует этот вид атак. Вы можете сохранить приложение в файле xss2.go и
запустить с помощью команды go run xss2.go.

Фрагмент 5: Хранимая XSS-атака.
        package main

import (
	"fmt"
	"log"
	"net/http"
	"strings"
	"sync"
)

var db []string
var mu sync.Mutex

var tmpl = `
<form action="/save">
  Message: <input name="message" type="text"><br>
  <input type="submit" value="Submit">
</form>
%v
`

func saveHandler(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	defer mu.Unlock()

	r.ParseForm()
	messages, ok := r.Form["message"]
	if !ok {
		http.Error(w, "missing message", 500)
	}

	db = append(db, messages[0])

	http.Redirect(w, r, "/", 301)
}

func viewHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")

	var sb strings.Builder
	sb.WriteString("<ul>")
	for _, message := range db {
		sb.WriteString("<li>" + message + "</li>")
	}
	sb.WriteString("</ul>")

	fmt.Fprintf(w, tmpl, sb.String())
}

func main() {
	http.HandleFunc("/", viewHandler)
	http.HandleFunc("/save", saveHandler)
	log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Чтобы увидеть атаку
XSS, перейдите по ссылке http://localhost:8080 и введите сообщение <script>alert(1);</script>.

Атака делится на две
фазы:

  • пейлоад сохраняется в хранилище данных в функции storeHandler;
  • когда страница визуализируется во ViewHandler, пейлоад добавляется к выходным данным.

XSS-атаки на основе DOM

Такие атаки не связаны с бекендом и происходят исключительно на стороне клиента. Они интересны тем, что современные веб-приложения перемещают логику к клиенту, а атаки происходят, когда пользователь напрямую манипулирует DOM. Хорошей новостью для злоумышленников является то, что DOM имеет широкий спектр способов эксплуатации, наиболее популярными из которых являются innerHTML и document.write.

Ниже приведен пример обслуживающего статический контент веб-приложения. Код тот же, что и в
примере с рефлективными XSS, но здесь атака будет происходить полностью на
стороне клиента. Вы можете сохранить приложение в файле xss3.go и запустить его командой go run xss3.go.

Фрагмент 6: Пример веб-приложения с XSS-атакой на основе DOM.
        package main

import (
    "fmt"
    "log"
    "net/http"
)

const content = `

<html>
   <head>
       <script>
          window.onload = function() {
             var params = new URLSearchParams(window.location.search);
             p = document.getElementById("content")
             p.innerHTML = params.get("message")
	     };
       </script>
   </head>
   <body>
       <p id="content"></p>
   </body>
</html>
`

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("X-XSS-Protection", "0")
    fmt.Fprintf(w, content)
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Чтобы увидеть эту
атаку, перейдите по ссылке
http://localhost:8080/?message=”<img src=1 onerror=alert(1);/>”. Обратите внимание, что вектор атаки немного
отличается и innerHTML не будет
выполнять скрипт напрямую, однако он добавит HTML-элементы, которые затем выполнят код на JavaScript. В приведенном примере добавляется элемент image, который запускает скрипт при возникновении ошибки (она всегда появляется, поскольку злоумышленник подставляет неверный источник).

Если хотите напрямую добавить
элемент скрипта, придется использовать другой приемник XSS. Замените
элемент script из фрагмента 6 элементом script из фрагмента 7 и перейдите
по следующей ссылке:
http://localhost:8080/?message=”<script>alert(1);</script>”. Атака сработает, потому что document.write принимает элементы
скрипта напрямую.

Фрагмент 7: Еще один пример атаки XSS на основе DOM.
        <script>
   window.onload = function() {
      var params = new URLSearchParams(window.location.search);
      document.open();
      document.write(params.get("message"));
      document.close();
   };
</script>
    

Связанные направления атак

Хотя обычно их не называют атаками XSS, существует несколько связанных направлений, о которых стоит упомянуть.

Content-type

Всему виной
неправильная настройка типа содержимого ответов HTTP. Это может произойти как
на уровне бекенда (ответ имеет неверный набор заголовков Content-Type), так и при попытке браузера проснифферить тип MIME.
Internet Explorer был особенно восприимчив к этому, и классическим примером
является служба загрузки изображений: злоумышленник может загрузить JavaScript вместо картинки.
Браузер видит, что тип контента был установлен на image/jpg, но пейлоад содержит скрипт – он выполняется, что приводит к атаке XSS.

Urlschemes

Следующий тип атаки –
активность через URL со схемой JavaScript. Представим веб-сайт, который
позволяет пользователю контролировать цель ссылки, как показано во фрагменте 8. В этом случае злоумышленник сможет предоставить
URL, выполняющий некий JavaScript с помощью нашей схемы.

Чтобы опробовать этот
тип атаки, можно сохранить приложение в файле xss4.go, запустить командой go run xss4.go и перейти по ссылке http://localhost:8080?link=javascript:alert(1).

Фрагмент 8: XSS-атака, введенная через схему URL-адресов.
        package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("X-XSS-Protection", "0")

    links, ok := r.URL.Query()["link"]
    if !ok {
        messages = []string{"example.com"}
    }
    fmt.Fprintf(w, `<html><p><a href="%v">Next</p></html>`, links[0])
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Избавление

Единого метода
решения данной проблемы не существует, иначе XSS не был бы
такой распространенной проблемой. Фундаментальная сложность вызвана отсутствием разделения между кодом и данными. Смягчение последствий XSS
обычно включает очистку входных данных (нужно убедиться, что они не
содержат кода), экранирование выходных данных (они также не должны содержать код) и реструктуризацию приложения таким образом, чтобы код
загружался из строго определенных конечных точек.

Валидация входных данных

Первая линия защиты – проверка входных данных. Убедитесь, что их формат соответствует ожидаемым характеристикам – эдакий белый список, гарантирующий отсутствие у приложения возможности принимать код.

Валидация
данных – сложная проблема. Не существует универсального инструмента или техники
для всех ситуаций. Лучше всего структурировать приложение таким образом, чтобы
оно требовало от разработчиков продумать тип принимаемых данных и обеспечить
удобное место, где можно разместить валидатор.

Хороший тон написания
приложений на Go состоит в том, чтобы не иметь никакой логики приложения в
обработчиках запросов HTTP, а вместо этого использовать их для анализа и проверки входных данных. Затем данные отправляются
в
обрабатывающую логику структуру. Обработчики запросов становятся
простыми и обеспечивают удобное централизованное расположение для контроля
правильности очистки данных.

На фрагменте 9
показано, как можно переписать saveHandler для приема символов ASCII [A-Za-z\.].

Фрагмент 9: Пример использования обработчиков HTTP-запросов для проверки данных.
        func saveHandler(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	messages, ok := r.Form["message"]
	if !ok {
		http.Error(w, "missing message", 500)
	}

	re := regexp.MustCompile(`^[A-Za-z\\.]+$`)
	if re.Find([]byte(messages[0]))) == "" {
		http.Error(w, "invalid message", 500)
	}
  
	db.Append(messages[0])

	http.Redirect(w, r, "/", 301)
}
    

Может показаться, что
это излишнее беспокойство, но чат-приложение принимает гораздо больше, чем ограниченный набор символов. Многие принимаемые приложениями данные достаточно структурированы: адреса, номера телефонов,
почтовые индексы и тому подобные вещи могут и должны быть
проверены.

Экранирование

Следующий шаг – экранирование вывода. В случае с нашим чатом все извлеченное из базы данных включалось непосредственно в выходной документ.

Одно и то же приложение
может быть гораздо безопаснее (даже если в него была произведена инъекция кода),
если экранировать все небезопасные выходные данные. Именно это делает пакет
html/template в Go. Использование языка шаблонов и
контекстно-зависимого синтаксического анализатора для экранирования данных до
их визуализации уменьшит вероятность выполнения вредоносного кода.

Ниже приведен пример
использования пакета html/template. Сохраните приложение в файле xss5.go, а затем выполните командой go run xss5.go.

Фрагмент 10: Использование экранирования для устранения хранимых XSS-атак.
        package main

import (
	"bytes"
	"html/template"
	"io"
	"log"
	"net/http"
	"sync"
)

var db []string
var mu sync.Mutex

var tmpl = `
<form action="/save">
  Message: <input name="message" type="text"><br>
  <input type="submit" value="Submit">
</form>
<ul>
{{range .}}
    <li>{{.}}</li>
{{end}}
</ul>`

func saveHandler(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	defer mu.Unlock()

	r.ParseForm()
	messages, ok := r.Form["message"]
	if !ok {
		http.Error(w, "missing message", 500)
	}

	db = append(db, messages[0])

	http.Redirect(w, r, "/", 301)
}

func viewHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")

	t := template.New("view")

	t, err := t.Parse(tmpl)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	var buf bytes.Buffer

	err = t.Execute(&buf, db)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	io.Copy(w, &buf)
}

func main() {
	http.HandleFunc("/", viewHandler)
	http.HandleFunc("/save", saveHandler)
	log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Опробуйте использованную ранее атаку XSS, перейдя по ссылке http://localhost:8080 и введите <script>alert(1);</script>.
Обратите внимание, что предупреждение не было вызвано.

Откройте консоль браузера и посмотрите на элемент li в DOM. Интерес представляют два
свойства: innerHTML и innerText.

Фрагмент 11: Проверка DOM при использовании экранирования.
        innerHTML: "<script>alert(1);</script>"
innerText: "<script>alert(1);</script>"
    

Обратите внимание, как
с помощью экранирования удалось четко разделить код и данные.

Content Security Policy

Content Security Policy (CSP) позволяет веб-приложениям определять набор доверенных источников для загрузки контента (например, скриптов). CSP можно использовать для разделения кода и данных, отказываясь от встроенных скриптов и загружая их только из определенных источников.

Написание CSP для
небольших автономных приложений является простой задачей – начните с политики,
которая по умолчанию запрещает все источники, а затем разрешите небольшой их набор.
Однако написать эффективный CSP для больших сайтов уже не так просто. Как
только сайт начинает загружать контент из внешних источников, CSP раздувается и
становится громоздким. Некоторые разработчики сдаются и включают директиву unsafe-inline, полностью разрушая теорию
CSP.

Чтобы упростить
написание CSP, в CSP3 вводится директива strict-dynamic.
Вместо того чтобы поддерживать большой белый список надежных источников,
приложение генерирует случайное число (nonce) каждый раз, когда запрашивается
страница. Этот nonce отправляется вместе с заголовками страницы и встроен в тег
script, что заставляет браузеры доверять этим скриптам с соответствующим nonce,
а также любым скриптам, которые они могут загрузить. Вместо
того, чтобы вносить скрипты в белый список и пытаться выяснить, какие еще сценарии они загружают, а затем пополнять белый список рекурсивно,
вам нужно достаточно внести в белый список импортируемый скрипт верхнего уровня.

Используя предложенный Google подход Strict CSP, рассмотрим простое приложение, принимающее
пользовательский ввод. Сохраните его в файле xss6.go, а затем выполните командой go run xss6.go.

Фрагмент 12: Пример CSP, смягчающего XSS-атаку.
        package main

import (
	"bytes"
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"strings"
)

const scriptContent = `
document.addEventListener('DOMContentLoaded', function () {
   var updateButton = document.getElementById("textUpdate");
   updateButton.addEventListener("click", function() {
      var p = document.getElementById("content");
      var message = document.getElementById("textInput").value;
      p.innerHTML = message;
   });
};
`

const htmlContent = `
<html>
   <head>
      <script src="script.js" nonce="{{ . }}"></script>
   </head>
   <body>
       <p id="content"></p>

       <div class="input-group mb-3">
         <input type="text" class="form-control" id="textInput">
         <div class="input-group-append">
           <button class="btn btn-outline-secondary" type="button" id="textUpdate">Update</button>
         </div>
       </div>

       <blockquote class="twitter-tweet" data-lang="en">
         <a href="https://twitter.com/jack/status/20?ref_src=twsrc%5Etfw">March 21, 2006</a>
       </blockquote>
       <script async src="https://platform.twitter.com/widgets.js"
         nonce="{{ . }}" charset="utf-8"></script>
   </body>
</html>
`

func generateNonce() (string, error) {
	buf := make([]byte, 16)
	_, err := rand.Read(buf)
	if err != nil {
		return "", err
	}

	return base64.StdEncoding.EncodeToString(buf), nil
}

func generateHTML(nonce string) (string, error) {
	var buf bytes.Buffer

	t, err := template.New("htmlContent").Parse(htmlContent)
	if err != nil {
		return "", err
	}

	err = t.Execute(&buf, nonce)
	if err != nil {
		return "", err
	}

	return buf.String(), nil
}

func generatePolicy(nonce string) string {
	s := fmt.Sprintf(`'nonce-%v`, nonce) 
	var contentSecurityPolicy = []string{
		`object-src 'none';`,
		fmt.Sprintf(`script-src %v 'strict-dynamic';`, s),
		`base-uri 'none';`,
	}
	return strings.Join(contentSecurityPolicy, " ")
}

func scriptHandler(w http.ResponseWriter, r *http.Request) {
	nonce, err := generateNonce()
	if err != nil {
		returnError()
		return
	}

	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
	w.Header().Set("Content-Security-Policy", generatePolicy(nonce))

	fmt.Fprintf(w, scriptContent)
}

func htmlHandler(w http.ResponseWriter, r *http.Request) {
	nonce, err := generateNonce()
	if err != nil {
		returnError()
		return
	}

	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Header().Set("Content-Security-Policy", generatePolicy(nonce))

	htmlContent, err := generateHTML(nonce)
	if err != nil {
returnError()
		return
	}

	fmt.Fprintf(w, htmlContent)
}

func returnError() {
http.Error(w, http.StatusText(http.StatusInternalServerError),
		http.StatusInternalServerError)
}

func main() {
	http.HandleFunc("/script.js", scriptHandler)
	http.HandleFunc("/", htmlHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}
    

Чтобы попытаться
использовать приложение, перейдите по ссылке: http://localhost:8080 и
попробуйте отправить <img src=1 onerror”alert(1)”/> как и раньше. Эта атака сработала бы и без CSP, но
поскольку CSP не допускает inline-скриптов, вы должны увидеть примерно такой вывод в консоли браузера:

«Отказано в выполнении встроенного обработчика событий, поскольку он нарушает следующую директиву CSP: “script-src ‘nonce-XauzABRw9QtE0bzoiRmslQ==’ ‘unsafe-inline’ ‘unsafe-eval’ ‘strict-dynamic’ https: http:” Обратите внимание, что ‘unsafe-inline‘ игнорируется, если в исходном списке присутствует либо хэш, либо значение nonce.»

Почему сценарий не
запустился? Рассмотрим
CSP подробнее.

Фрагмент 13: Базовый CSP. Nonce повторно генерируется для каждого запроса.
        script-src 'strict-dynamic' 'nonce-XauzABRw9QtE0bzoiRmslQ==';
object-src 'none';
base-uri 'none';
    
Что делает эта политика? Директива script-src включает strict-dynamic и значение nonce, используемое для загрузки скриптов. Это означает, что единственные скрипты, которые будут загружены, находятся в script elements, где nonce включен в атрибут, а значит inline-скрипты не загрузятся. Последние две директивы препятствуют загрузке плагинов и изменению базового URL приложения.

Основная сложность
использования этого подхода заключается в необходимости генерировать nonce и инжектить его в заголовки при каждой загрузке страницы. После этого шаблон
может быть применен ко всем загружаемым страницам.

Соответствующие методы устранения

Content-Type

Вы
должны не только устанавливать свой Content-Type, но и следить, чтобы браузеры не
пытались автоматически определить тип контента. Для
этого используйте заголовок: X-Content-Type-Options:
nosniff
.

Virtual doms

Хотя виртуальные домены
не являются функцией безопасности, использующие их современные фреймворки (
React и Vue) могут помочь смягчить атаки XSS на основе DOM.

Эти фреймворки создают DOM параллельно с тем, который находится в браузере, и сравнивают их. Отличающуюся часть DOM браузера они обновляют. Для этого необходимо создать виртуальный DOM, что приведет к уменьшению использования клиентами innerHTML и подтолкнет разработчиков к переходу на innerText.

React требует
использования атрибута
dangerouslySetInnerHTML, в то время как создатели Vue предупреждают, что использование
innerHTML может привести к появлению уязвимостей.

Заключение

Если вы дочитали до
конца, у вас может появиться желание разобраться, как работают браузеры, что такое ошибки
XSS и насколько важно понимать, как от них избавиться.
XSS трудно искоренить, поскольку приложения становятся все больше и все сложнее. Применяя упомянутые в статье методы, можно сделать жизнь злоумышленников трудной.

Удачи в борьбе и учебе!

Дополнительные материалы:

07
Мар
2021

🕸 Самые удобные браузеры для веб-разработчиков

Правильный выбор инструментов очень важен. В небольшом обзоре мы рассмотрим наиболее продвинутые браузеры для разработки фронтенда.

Если какое программное обеспечение разработчики…

03
Мар
2021

🎨 50 онлайн-инструментов для дизайна: веб-сайты и мобильные приложения

С помощью этих инструментов без специальных знаний можно создать обложку для социальной сети, иконку, мокап и задизайнить сайт через визуальный редактор. В списке есть ресурсы с легальными и бесплатными изображениями, векторным артом, фотог…

27
Фев
2021

🕸 Инструменты веб-разработчика: сервисы и расширения, о которых в 2021 году должен знать каждый

Оперативно проверить сайт или приложение, найти баги и улучшить проект веб-разработчикам помогут специальные сервисы и расширения. Мы собрали более 25 инструментов, упрощающих жизнь программисту.

25
Фев
2021

☕ 25 полезных плагинов и библиотек JavaScript, о которых вы можете не знать

На «ванильном» JavaScript сейчас почти никто не пишет, но кроме мощных фреймворков есть и небольшие библиотеки с интересными возможностями. В наш список попали наиболее полезные из них, часто применяемые веб-разработчиками.

04
Фев
2021

☕ Альтернативные фреймворки JavaScript: есть ли жизнь за пределами большой тройки?

Мы разберем не такие известные, но довольно часто используемые инструменты JavaScript, которые помогут облегчить жизнь веб-разработчика. Необязательно зацикливаться только на трех основных фреймворках.

29
Янв
2021

🕸 Из джунов в мидлы: зачем фронтенд-разработчику информационная безопасность?

Совместно с Яндекс.Практикумом рассказываем о популярных уязвимостях во фронтенде, о которых должен знать мидл или тот, кто хочет им стать. Также: лекции, курсы, XSS- SQL-сканеры и awesome-списки.

29
Янв
2021

☕ 7 советов изучающему Vue.js новичку

Vue.js – один из самых популярных фреймворков для фронтенд-разработки и один из самых простых в изучении. Рассказываем, на что нужно обратить внимание желающим освоить его с нуля новичкам.

Vue.js относительно несложен в изучении, но обладает мощной функциональностью и отличной производительностью. Чтобы немного разобраться со всеми нюансами разработки на Vue и продвинуться на шаг вперед, стоит ознакомиться с рядом важных особенностей фреймворка:

1. Поймите принцип реактивности

Реактивность – это простая концепция, необходимая для интерфейсной разработки. Вот небольшой пример:

<h1> {{name}} </h1>

При изменении значения name пользовательский интерфейс будет соответствующим образом обновлен. Однако это слишком просто и не раскрывает всей сути.

        <template>
  <div>
    <p>{{ myObject.message }}</p>
  </div>
</template>

    
          export default {
data() {
   return {
     myObject: {}
   }
},
mounted() {
   this.myObject.message = 'Hello';
}
  };
    

В приведенном коде мы определяем myObject как пустой объект в функции data( ), а затем присваиваем myObject.message значение. Однако в {{ myObject.message }} при получении этого значения ничего не отображается. Причина в том, что Vue не знает о существовании свойств myObject.message и поэтому не может среагировать на изменения его значения. Как это исправить?

Есть несколько способов убедиться, что Vue видит изменения myObject.message. Самый простой – инициализировать его с пустым или нулевым значением:

myObject: { message: ' ' }

Если myObject.message присутствует в функции data( ), то Vue определит это и среагирует на изменения его значения, обновляя пользовательский интерфейс.

Еще один способ убедиться в отзывчивом UI – обновить весь объект myObject:

this.myObject = {message: 'Hello world'}.

В этом случае, данные на странице также будут изменяться.

Хорошо понимая реактивность, вы можете делать следующее:

  1. Быстро находить ошибки в приложениях, при которых пользовательские данные не обновляется;
  2. Определять, когда элементы изменяются, а когда отображаются повторно;
  3. Понимать как обрабатываются свойства объекта в приложении Vue.

2. Постройте оптимальную связь между элементами

Как у новичка, у меня все время возникали разные вопросы по поводу передачи данных от дочернего к родительскому компоненту, или, например, как мне убедиться, что дочерний компонент обновляется при изменениях в родительском?

Самый простой способ осуществить связь между ними – использовать реквизит props. Реквизит передает данные от родителя к потомку. Они могут быть разных типов: строки, логические значения, массивы и т. д.

<component: message = "myObject.message" />

Свойства формируют односторонний поток данных: всякий раз, когда в myObject.message изменяется родительский компонент, свойство message будет соответствующим образом обновляться. Делать в обратном порядке – неверно!

Вы не должны изменять значение props в дочерних компонентах. Если же вы все-таки попытаетесь изменить их, Vue предупредит, что так делать не стоит. Если вам нужно передать значения от дочернего компонента родительскому, нужно сгенерировать событие. Например, следующим образом:

         <template>

  <div>

<child-component @button-clicked="processUpdate" />

  </div>

</template>
    
          export default {
   methods: {
    processUpdate(value) {
     // This will output: true
     console.log(value);
    }
   }
  }
    
        <!-- Child Component -->
<template>
  <button @click="notify">Notify Parent</button>
</template>
    
          export default {
   methods: {
    notify() {
        // вместо true мы можем написать любое значение, например object
        this.$emit('button-clicked', true);
      }
    }
  }
    

Используйте $emit для передачи одного или нескольких значений или событий от дочерних компонентов к родительским. Значения могут быть любого типа.

3. Решите проблемы с производительностью

Задача разработчиков программного обеспечения – обеспечить быструю и бесперебойную работу своих проектов. Однако существует несколько проблем, о которых стоит упомянуть.

Утечки памяти (memory leaks) – распространенная проблема производительности веб-приложений. Хотя сам Vue не вызывает их без причины, это может произойти при включении сторонних библиотек или из-за ошибочного кода. Особенно важно избегать утечек памяти при создании одностраничных приложений (SPA), потому что по умолчанию пользователи не обновляют вкладки в браузере и приложение начинает собирать мусор.

Не вдаваясь в подробности, у Vue есть официальное руководство по предотвращению утечек памяти. Если вы хотите стать разработчиком, стоит внимательно его прочитать.

Внешние библиотеки, вроде React и Vue, делают нас ленивыми. Мы не совсем понимаем, что происходит в процессе рендеринга – просто используем v-for и ожидаем, что все будет работать. Есть много неожиданных причин, по которым приложение Vue может иметь проблемы с производительностью, чрезмерно затрачивая ресурсы на рендеринг:

  1. Создается слишком много элементов DOM;
  2. Страница обновляется слишком часто;
  3. Используются сторонние компоненты, на рендеринг которых требуется много времени.

Все вышеперечисленное нужно учитывать при дальнейшей работе с фреймворком.

Оптимизируйте обработку событий, а также используйте функции-ограничители throttle или debounce. Дело в том, что прокрутка, наведение курсора мыши и другие события могут запускать обработчик десятки раз в секунду. Это больше относится к JavaScript в целом, однако про такие вещи не стоит забывать.

4. Изучите экосистему Vue

Чтобы продвинуться в разработке Vue, вам нужно будет узнать о составляющих его экосистему пакетах. Для большинства приложений потребуется по крайней мере один из них, но для освоения фреймворка следует ознакомиться со всеми тремя.

Vuex

Большинство современных приложений (особенно SPA) полагаются на своего рода управление состоянием. Vuex – это официальная библиотека управления состоянием для приложений Vue, хорошо интегрированная с официальными инструментами разработки. Она служит централизованным хранилищем, к которому все компоненты могут получить доступ для работы с глобальными данными приложения.

Vue Router

Использование Vue без Vue Router – это редкость, но в некоторых проектах такое встречается. Более подробная информация о нем доступна на официальном сайте. Я просто упомяну, что изучение vue-router является обязательным условием для создания большинства современных SPA-приложений.

Vue SSR

Потребность в рендеринге на стороне сервера обычно возникает, когда производительность приложения начинает ухудшаться. SSR помогает оптимизировать время отображения контента, особенно на медленных устройствах или при медленном Интернете, а также полезно для SEO.

5. Хорошо организуйте приложение

Одна из ошибок, которые допускают неопытные разработчики Vue – плохо структурированные компоненты. Часто приложения Vue содержат только один или несколько мегакомпонентов, которые выполняют всю работу. Это неправильный подход.

Существует несколько причин, по которым использование небольших компонентов идет на пользу вашему приложению:

  1. Вы можете повторно их использовать в проекте;
  2. Ваш код становится более чистым и организованным;
  3. Vue эффективнее отображает дочерние компоненты в циклах v-for.

Если вам предстоит собеседование по Vue с тестовым кодингом, хорошо организованное приложение наверняка произведет впечатление на интервьюеров.


6. Используйте миксины

Миксины позволяют сделать следующее:

  1. Повторно использовать код в проекте;
  2. Писать код только один раз по принципу DRY(Don’t Repeat Yourself);
  3. Упростить рефакторинг и сопровождение кода.

Миксины предоставляют нам свободу действий. С их помощью вы можете создать переменную, включающую общие функции, условия и свойства компонентов. Возможности безграничны, но смысл понятен: использование миксинов – хорошая практика.

7. Применяйте расширенные директивы

Помимо v-for, v-if и других общих директив, Vue имеет некоторые директивы, которые являются менее распространенными, но очень полезными.

***

Используйте v-once для вывода значения однократно, если хотите сделать его нереактивным. При последующих его изменениях, значение больше не будет отображаться, что может помочь вам оптимизировать производительность в определенных сценариях:

<span v-once> {{имя}} </span>

***

Для событий v-on есть несколько очень удобных модификаторов. Очень часто используется @click.prevent, который автоматически вызывает preventDefault() у элемента на странице.

<a href="#" @click.prevent="processClick"> Нажми меня </a>

Полный список модификаторов событий можно увидеть тут .

***

Vue позволяет вам регистрировать собственные пользовательские директивы, как описано в документации . Если вы обнаружите, что повторяете одно и то же поведение, стоит создать такую настраиваемую директиву, которую можно использовать повторно. Главное тут – делать их только при наличии реальной потребности, а не на один или два раза.

***

Vue.js довольно прост в изучении, но с самого начала стоит убедиться, что вы делаете все правильно. Для самопроверки уделяйте побольше времени официальной документации и у вас все получится. Удачи в освоении этого замечательного инструмента!

26
Янв
2021

🕸 Обучение веб-разработке: полезные сервисы, о которых в 2021 году должен знать каждый

Сервисы для обучения веб-разработке в открытом доступе позволяют освоить теорию и получить базовые практические навыки программирования. Мы пишем о них довольно часто – читайте обзор ресурсов, актуальных в 2021 году.

25
Янв
2021

Как сделать — фиксированный/липкий заголовок

Как создать фиксированный заголовок при прокрутке Для удобства работы с сайтом часто разработчики используют фиксированную шапку сайта. Как сделать липки заголовок? достаточно просто, рассмотрим на примере: Шаг 1) добавить HTML: Пример <div class=»header» id=»myHeader»> <h2>My Header</h2> </div> Шаг 2) добавить CSS: Пример /* Style the header */ .header { padding: 10px 16px; background: #555; color: #f1f1f1; }/* Page content */ .content { padding: 16px; } /* The sticky… Читать далее »

Запись Как сделать — фиксированный/липкий заголовок впервые появилась Индиго.

22
Янв
2021

🕸 Что ждет профессию веб-разработчика в 2021 году: мнения экспертов

Технологии играют важную роль в повседневной жизни и применяются везде – от простых решений до инновационных продуктов. На основе мнений экспертов мы рассмотрим перспективы индустрии веб-разработки в наступившем 2021 году.

20
Янв
2021

⚛️ 12 советов по внедрению TypeScript в React-приложениях

React и TypeScript – две потрясающие технологии, используемые в наши дни множеством разработчиков. В этой статье мы постарались собрать лучшие практики внедрения TypeScript в React-приложениях.

Зачем использовать TypeScript в React?

React – это JavaScript-библиотека с открытым исходным кодом для создания пользовательских интерфейсов, а TypeScript – типизированное надмножество JavaScript. Объединяя их, мы создаем пользовательские интерфейсы, используя типизированную версию JavaScript. Это означает большую безопасность и меньшее количество ошибок, отправляемых во внешний интерфейс.

Не существует единственного правильного способа написания кода React с использованием TypeScript. Как и в случае с другими технологиями, если ваш код компилируется и работает, вы, вероятно, что-то сделали правильно.

Подготовка компонентов к совместному использованию с помощью TypeScript

<span>просмотр общих компонентов React в bit.dev</span>
просмотр общих компонентов React в bit.dev

Bit.dev стал популярной альтернативой традиционным библиотекам компонентов, поскольку он предлагает способ собрать и поделиться отдельными компонентами из любой кодовой базы.

Создавая проекты с использованием React с TS, вы убедитесь, что ваши компоненты легко понятны другим разработчикам. Это отличный способ написать поддерживаемый код и оптимизировать совместную работу команды.

Настройка проекта и tsconfig.json

Самый быстрый способ запустить приложение React/TypeScript – использовать create-react-app с шаблоном TypeScript:

        npx create-react-app my-app --template typescript
    

tsconfig.json это файл конфигурации TypeScript. Файл содержит первоначальные настройки, ниже приведем несколько параметров с пояснениями:

tsconfig.json
        {

  "compilerOptions": {

    "target": "es5", // Укажите целевую версию ECMAScript

    "lib": [

      "dom",

      "dom.iterable",

      "esnext"

    ], // Список файлов библиотеки для включения в компиляцию

    "allowJs": true, // Разрешить компиляцию файлов JavaScript

    "skipLibCheck": true, // Пропустить проверку типов всех файлов объявлений

    "esModuleInterop": true, // Отключает импорт пространства имен (импорт * как fs из "fs") и включает импорт в стиле CJS / AMD / UMD (импорт fs из "fs")

    "allowSyntheticDefaultImports": true, // Разрешить импорт по умолчанию из модулей без экспорта по умолчанию

    "strict": true, // Включить все параметры строгой проверки типов

    "forceConsistentCasingInFileNames": true, // Запрещаем ссылки с несогласованным регистром на один и тот же файл.

    "module": "esnext", // Указываем генерацию кода модуля

    "moduleResolution": "node", // Разрешить модули в стиле Node.js

    "isolatedModules": true, // Безоговорочно генерировать импорт для неразрешенных файлов

    "resolveJsonModule": true, // Включить модули, импортированные с расширением .json

    "noEmit": true, // Не выводить вывод (то есть не компилировать код, а только выполнять проверку типа)

    "jsx": "react", // Поддержка JSX в файлах .tsx

    "sourceMap": true, // Создание соответствующего файла .map

    "declaration": true, // Создаем соответствующий файл .d.ts

    "noUnusedLocals": true, // Сообщать об ошибках на неиспользуемых локальных объектах

    "noUnusedParameters": true, // Сообщаем об ошибках неиспользуемых параметров

    "incremental": true, // Включить инкрементную компиляцию путем чтения/записи информации из предыдущих компиляций в файл на диске

    "noFallthroughCasesInSwitch": true // Сообщать об ошибках для случаев падения в инструкции switch

  },

  "include": [

    "src/**/*" // *** Файлы TypeScript должны ввести проверку ***

  ],

  "exclude": ["node_modules", "build"] //  *** Файлы, которые не нужно вводить, проверять ***

}
    

Дополнительные рекомендации исходят от сообщества response-typescript-cheatsheet, а объяснения взяты из документации по параметрам компилятора в официальном справочнике TypeScript.

Enums

Enums определяет набор связанных констант как часть единой сущности.

        //...

/** A set of groupped constants */

enum SelectableButtonTypes {

    Important = "important",

    Optional = "optional",

    Irrelevant = "irrelevant"

}

interface IButtonProps {

    text: string,

    /** The type of button, pulled from the Enum SelectableButtonTypes */

    type: SelectableButtonTypes,

    action: (selected: boolean) => void

}

const ExtendedSelectableButton = ({text, type, action}: IButtonProps) => {

    let [selected, setSelected]  = useState(false)

    return (<button className={"extendedSelectableButton " + type + (selected? " selected" : "")} onClick={ _ => {

        setSelected(!selected)

        action(selected)

    }}>{text}</button>)

}

/** Exporting the component AND the Enum */

export { ExtendedSelectableButton, SelectableButtonTypes}
    

Использование Enums:

        import React from 'react';

import './App.css';

import {ExtendedSelectableButton, SelectableButtonTypes} from './components/ExtendedSelectableButton/ExtendedSelectableButton'

const App = () => {

  return (

    <div className="App">

      <header className="App-header">

        <ExtendedSelectableButton type={SelectableButtonTypes.Important} text="Select me!!" action={ (selected) => {

          console.log(selected) 

        }} />       

      </header>

    </div>

  );

}

export default App;
    

Интерфейсы и типы

Что следует использовать – интерфейсы или псевдонимы типов? Хотя эти сущности концептуально различны, на деле они очень похожи:

  • обе могут быть продлены;
        //расширение интерфейсов

interface PartialPointX { x: number; }

interface Point extends PartialPointX { y: number; }

//расширение типов

type PartialPointX = { x: number; };

type Point = PartialPointX & { y: number; };

// Интерфейс расширяет тип 

type PartialPointX = { x: number; };

interface Point extends PartialPointX { y: number; }

//Псевдоним типа расширяет интерфейсы

interface PartialPointX { x: number; }

type Point = PartialPointX & { y: number; };
    
  • обе могут использоваться для определения формы объектов;
        //определяем интерфейс для объектов

interface Point {

  x: number;

  y: number;

}

//также используем типы

type Point2 = {

  x: number;

  y: number;

};
    
  • обе они могут быть реализованы одинаково;
        //реализация интерфейса

class SomePoint implements Point {

  x: 1;

  y: 2;

}

//Реализация псевдонима типа

class SomePoint2 implements Point2 {

  x: 1;

  y: 2;

}

type PartialPoint = { x: number; } | { y: number; };

// Единственное, что вы не можете сделать: реализовать тип объединения

class SomePartialPoint implements PartialPoint {

  x: 1;

  y: 2;

}
    

Единственная функция интерфейсов, которую не делают псевдонимы типов – это объединение деклараций.

Расширение элементов HTML

Иногда ваши компоненты работают как собственные HTML-элементы. В таких случаях лучше определить тип компонента как собственный элемент HTML или его расширение.

        function eventHandler(event: React.MouseEvent<HTMLAnchorElement>) {

    console.log("TEST!")

}

const ExtendedSelectableButton = ({text, type, action}: IButtonProps) => {

    let [selected, setSelected]  = useState(false)

    return (<button className={"extendedSelectableButton " + type + (selected? " selected" : "")} onClick={eventHandler}>{text}</button>)

}
    

Типы событий

React имеет собственный набор событий, поэтому вы не можете напрямую использовать события HTML. Однако у вас есть доступ к событиям пользовательского интерфейса. Убедитесь, что ссылаетесь на них напрямую или просто не забудьте импортировать их из React следующим образом:

        import React, { Component, MouseEvent } from 'react';
    

Мы также можем использовать для ограничения элементов Generics с конкретным обработчиком событий.

Также можно применять объединения, чтобы разрешить повторное использование обработчика несколькими компонентами:

        / ** Это позволит вам использовать этот обработчик событий как для якорей, так и для элементов кнопок * /

function eventHandler(event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) {

    console.log("TEST!")

}
    

Определение интегрированного типа

Стоит упомянуть файлы index.d.ts и global.d.ts в React. Оба они устанавливаются, когда вы добавляете React в свой проект. Эти файлы содержат определения типов и интерфейсов: чтобы понять свойства или доступность конкретного типа, вы можете открыть эти файлы и просмотреть их содержимое.

<span> index.d.ts</span>
index.d.ts

Там вы увидете небольшой раздел файла index.d.ts, показывающий подписи для функции createElement.

ESLint/Prettier

Чтобы гарантировать, что ваш код соответствует правилам проекта, а стиль согласован, рекомендуется настроить ESLint и Prettier:

  • Установите необходимые зависимости для разработчиков:
        yarn add eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react --dev
    
  • Создайте файл .eslintrc.js в корне и добавьте следующее:
.eslintrc.js
        module.exports =  {

  parser:  '@typescript-eslint/parser',  // Указывает парсер ESLint

  extends:  [

    'plugin:react/recommended',  // Использует рекомендуемые правила из @eslint-plugin-react

    'plugin:@typescript-eslint/recommended',  // Использует рекомендуемые правила из @typescript-eslint/eslint-plugin

  ],

  parserOptions:  {

  ecmaVersion:  2018,  //Позволяет анализировать современные функции ECMAScript

  sourceType:  'module',  //Разрешает использование импорта

  ecmaFeatures:  {

    jsx:  true,  // Разрешает анализ JSX

  },

  },

  rules:  {

    // Место для указания правил ESLint. Может использоваться для перезаписи правил, указанных в расширенных конфигах

    // например "@ typescript-eslint / явный-возвращаемый-тип-функции": "выкл.",

  },

  settings:  {

    react:  {

      version:  'detect',  // Указывает eslint-plugin-react автоматически определять версию React для использования

    },

  },

};
    
  • Добавьте зависимости Prettier:
        yarn add prettier eslint-config-prettier eslint-plugin-prettier --dev
    
  • Создайте файл .prettierrc.js в корне и добавьте в него следующий код:
.prettierrc.js
        module.exports =  {

  semi:  true,

  trailingComma:  'all',

  singleQuote:  true,

  printWidth:  120,

  tabWidth:  4,

};
    
  • Обновите файл .eslintrc.js:
.eslintrc.js
        module.exports =  {

  parser:  '@typescript-eslint/parser',  // Задает парсер ESLint

  extends:  [

    'plugin:react/recommended',  // Использует рекомендуемые правила из @ eslint-plugin-react

    'plugin:@typescript-eslint/recommended',  // Использует рекомендуемые правила из @typescript-eslint/eslint-plugin

+   'prettier/@typescript-eslint',  // Использует eslint-config-prettier для отключения правил ESLint из @typescript-eslint/eslint-plugin, которые будут конфликтовать с prettier

+   'plugin:prettier/recommended',  // Включает eslint-plugin-prettier и отображает более красивые ошибки как ошибки ESLint. Убедитесь, что это всегда последняя конфигурация в массиве extends.

  ],

  parserOptions:  {

  ecmaVersion:  2018,  // Позволяет анализировать современные функции ECMAScript

  sourceType:  'module',  // Разрешает использование импорта

  ecmaFeatures:  {

    jsx:  true,  //Разрешает анализ JSX

  },

  },

  rules:  {

     // Место для указания правил ESLint. Может использоваться для перезаписи правил, указанных в расширенных конфигах

    // например "@typescript-eslint/явный-тип-возврата-функции": "выкл.",

  },

  settings:  {

    react:  {

      version:  'detect',  // Указывает eslint-plugin-react автоматически определять версию React для использования

    },

  },

};
    

Расширения и настройки кода VS

Следующий шаг по улучшению – автоматическое исправление и предварительная настройка кода при сохранении.

Установите ESLint и Prettier для VS Code. Это позволит ESLint легко интегрироваться с вашим редактором.

Затем обновите настройки, добавив следующий код в свой .vscode/settings.json:

.vscode/settings.json
        {

    "editor.formatOnSave": true

}
    

Хуки

Хуки, вроде useState, получают параметр и правильно возвращают состояние и функцию для его установки.

Вы можете принудительно установить тип или интерфейс начального значения состояния, например, так:

        const [user, setUser] = React.useState<IUser>(user);
    

Однако, если начальное значение для вашего хука потенциально может быть null, то приведенный выше код не сработает. Для этих случаев TypeScript позволяет установить дополнительный тип:

        const [user, setUser] = React.useState<IUser | null>(null);

// later...

setUser(newUser);
    

Обработка событий формы

Один из наиболее распространенных случаев – это правильный ввод using в поле ввода onChange. Вот пример:

        import React from 'react'

const MyInput = () => {

  const [value, setValue] = React.useState('')

  // The event type is a "ChangeEvent"

  // We pass in "HTMLInputElement" to the input

  function onChange(e: React.ChangeEvent<HTMLInputElement>) {

    setValue(e.target.value)

  }

  return <input value={value} onChange={onChange} id="input-example"/>

}
    

Расширение свойств компонентов

Можно расширить свойства, объявленные для одного компонента, и задействовать их в другом. Давайте сначала посмотрим, как использовать type:

        import React from 'react';

type ButtonProps = {

    /** the background color of the button */

    color: string;

    /** the text to show inside the button */

    text: string;

}

type ContainerProps = ButtonProps & {

    /** the height of the container (value used with 'px') */

    height: number;

}

const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {

  return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>

}
    

Если вы использовали interface, то можно применить extends, чтобы расширить его, но придется внести пару изменений:

        import React from 'react';

interface ButtonProps {

    /** the background color of the button */

    color: string;

    /** the text to show inside the button */

    text: string;

}

interface ContainerProps extends ButtonProps {

    /** the height of the container (value used with 'px') */

    height: number;

}

const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {

  return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>

}
    

Сторонние библиотеки

Мы часто включаем сторонние библиотеки в проекты React и TypeScript. В таких случаях стоит посмотреть, есть ли пакет @types с определениями типов:

        #yarn

yarn add @types/<package-name>

#npm

npm install @types/<package-name>
    

Пространство имен @types зарезервировано для определений типов пакетов. Они живут в репозитории под названием DefinitherTyped, который частично поддерживается командой TypeScript, а частично – сообществом.

Итоги

Совместное использование React и TypeScript требует некоторого обучения из-за объема информации, но в долгосрочной перспективе затраты времени окупаются. В своей публикации мы опирались на лучшие практики из статей Джо Превайта, Фернандо Дольо и Мартина Хохеля. Мы постарались осветить самые полезные приемы и моменты, которые помогут использовать TypeScript как часть инструментальной цепочки React. Больше информации вы найдете в официальном справочнике к TypeScript или в публикациях «Библиотеки программиста».

14
Янв
2021

🕸 Что не так с веб-сборкой?

Ее потенциал и перспективы огромны. Что же пошло не так? Почему не все используют WASM? Попробуем ответить на эти вопросы.

Перевод публикуется с сокращениями, автор оригинальной ст…

09
Янв
2021

🕸 Обучение веб-разработке: именование классов и идентификаторов, методология БЭМ

Существует множество методологий веб-разработки, направленных на уменьшение нагрузки на CSS, организацию сотрудничества в команде и поддержку большой кодовой базы. В этой статье речь пойдет о наиболее популярной из них – БЭМ.