Перетяжка, Дом карьеры
Перетяжка, Дом карьеры
Перетяжка, Дом карьеры

5 шагов для защиты backend: чек-лист от уязвимостей

В мире, где киберугрозы становятся всё более изощрёнными, защита backend-приложений от уязвимостей становится ключевым элементом безопасности. В этой статье мы представим пять основных шагов, которые помогут вам минимизировать риски и сделать серверную часть более защищённой. Семён Шаплыгин, Senior Software Developer в Yandex и эксперт Эйч, поделится своим опытом и даст практические советы, которые помогут избежать самых распространённых ошибок и уязвимостей.

3К открытий11К показов
5 шагов для защиты backend: чек-лист от уязвимостей

Защита backend — это не вопрос хорошего кода. Это вопрос выживания. Серверная часть приложения — не витрина, а сердце системы: здесь обрабатываются конфиденциальные данные и управляется бизнес-логика. Уязвимость в backend — не просто ошибка, а потенциальная утечка данных, финансовые потери, а в худших случаях — полный контроль злоумышленников над системой.

Мы живем в мире, где враг известен. Крупные компании и профильные сообщества ежегодно публикуют отчеты о найденных уязвимостях. Изучая их, можно увидеть, что атаки редко бывают новыми — чаще всего разработчики снова и снова наступают на одни и те же грабли.

Я — Семен Шаплыгин, Senior Software Developer в Yandex и экспертом Эйч Навыки. В статье расскажу о пяти главных шагах для защиты backend-приложений от уязвимостей. Поделюсь инсайтами и практическими советами, которые позволят избежать распространённых ошибок и уберечь систему от возможных атак.

Аудит безопасности: с чего начать

Безопасность — не состояние, а процесс. И если этот процесс не встроен в работу команды, значит, в системе уже есть уязвимости, о которых вы просто не знаете.

Подход к выявлению слабых мест должен включать несколько ключевых практик (запомните их, как мантру):

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

Безопасность начинается с осознания слабых мест. А теперь разберем шаги, которые помогут эти слабые места закрыть.

Чек-лист защиты backend: 5 ключевых шагов

Шаг 1. Защита от XSS: экранируйте ввод и вывод

Проблема:

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

Что делать:

Для предотвращения XSS необходимо экранировать выводимые данные, используя функции, которые преобразуют специальные символы в HTML-сущности. Например, для шаблонов Go можно использовать html/template вместо text/template, что обеспечивает автоматическое экранирование данных.

Пример:

			import "text/template"

var tmpl = template.Must(template.ParseGlob("templates/*"))

func (a *App) indexHandler(c echo.Context) error {
  rows, _ := a.db.Query("SELECT title, content FROM posts")
  defer rows.Close()

  posts := getPosts(rows)
	
  // Вставляем посты напрямую в HTML
  templates.Get().ExecuteTemplate(c.Response().Writer, "index.html", posts)
}
		

Мы выводим пользовательский ввод (заголовок и содержание поста) напрямую в HTML без экранирования, что позволяет внедрить JavaScript-код, если он по каким-то причинам ранее попал в базу. Чтобы это исправить, достаточно использовать библиотеку "html/template"

			import "html/template"

var tmpl = template.Must(template.ParseGlob("templates/*"))

		

Шаг 2. Защита от CSRF: используйте токены

Проблема:

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

Что делать:

Пользователь открывает форму для создания поста. Если злоумышленник заставит пользователя отправить запрос POST /post, например, через <img src="http://example.com/create?title=Hacked&content=Malicious" />, сервер выполнит этот запрос, не проверяя, кто его отправил.

Для защиты от CSRF необходимо использовать специальные токены, которые уникальны для каждого запроса. Они проверяются сервером, и если токен отсутствует или недействителен, сервер отклоняет запрос. Один из вариантов — привязка CSRF-токенов к пользовательской сессии, что исключает возможность использования токенов другого пользователя злоумышленником. Для вашего удобства — библиотеки по типу gorilla/csrf, которые обеспечивают защиту от CSRF-атак.

Дополнительно можно повысить уровень безопасности путем проверки заголовков Referer или Origin в POST-запросах, чтобы убедиться, что запрос поступил с доверенного источника. Также полезно ограничить CORS (Cross-Origin Resource Sharing), чтобы только определенные сайты могли отправлять запросы к вашему серверу.

Пример:

			func (a *App) postCreateHandler(c echo.Context) error {
   title := r.FormValue("title")
   content := r.FormValue("content")

   _, err := a.db.Exec("INSERT INTO posts (title, content) VALUES (?, ?)", title, content)
   if err != nil {}

   return c.Redirect(http.StatusSeeOther, "/")
}

		

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

Измененный метод для страницы создания поста:

			func (a *App) postPageHandler(c echo.Context) error {
   token := generateCSRFToken()

   templates.Get().ExecuteTemplate(c.Response().Writer, "post.html", token)

   return nil
}
		

Измененный метод сохранения поста:

			func (a *App) postCreateHandler(c echo.Context) error {
   token := с.FormValue("csrf_token")
   if !validateCSRFToken(token) {
       return c.String(http.StatusForbidden, "invalid request method")
   }

   // Далее аналогичная логика из примера с ошибкой
}

		

Шаг 3. Контроль доступа: избегайте уязвимостей IDOR/Broken ACL

Проблема:

Все это — недостатки контроля доступа IDOR/Broken ACL. Данная проблема возникает, когда приложение позволяет пользователю получить доступ к ресурсам или действиям без надлежащей проверки прав. Это часто происходит, если доступ к ресурсам идентифицируется по значениям ID, которые можно предсказать или изменить.

Что делать:

Чтобы избежать уязвимостей IDOR и Broken ACL, нужно проверять права пользователя перед предоставлением доступа к ресурсам. Рассмотрим пример со следующим сниппетом кода.

Пример:

На сайт добавили возможность просматривать конкретный пост, если его передать в путь запроса /post/{{post_id}}

			func (a *App) getUserPostsHandler(c echo.Context) error {
   postID, _ := strconv.Atoi(r.URL.Query().Get("post_id"))

   rows, err := a.db.Query("SELECT title, content FROM posts WHERE post_id = ? ORDER BY created_at DESC", postID)
   if err != nil {
       return c.String(http.StatusInternalServerError, "internal server error")
   }
   defer rows.Close()

   post := getPost(rows)

   templates.Get().ExecuteTemplate(c.Response().Writer, "user_posts.html", posts)

   return nil
}

		

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

Чтобы это исправить, достаточно в запросе указать признак пользователя:

			
func (a *App) getUserPostsHandler(c echo.Context) error {
 userID, _ := strconv.Atoi(c.QueryParam("user_id"))
 postID, _ := strconv.Atoi(c.QueryParam("post_id"))

 const query = `
  SELECT title, content 
  FROM posts 
  WHERE user_id = ? AND post_id = ? 
  ORDER BY created_at DESC
 `
 rows, err := a.db.Query(query, userID, postID)
 //  Далее аналогичная логика из примера с ошибкой
}

		

Дополнительно стоит пересмотреть использование числовых идентификаторов постов, так как целочисленные значения можно легко перебирать. Например, злоумышленник может просто увеличить post_id и попытаться получить информацию о постах с большими ID, чтобы оценить популярность вашего сервиса по сравнению с конкурентами. Чтобы избежать этой проблемы, рекомендуется использовать UUID версии v7. Этот тип идентификаторов спроектирован так, чтобы хорошо работать с базами данных и не поддаваться простому перебору.

Шаг 4. Не храните конфиденциальную информацию в открытом виде

Проблема:

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

Что делать:

Для того, чтобы избежать раскрытия конфиденциальных данных, нужно не включать их в ответы API. Достаточно уменьшить уровень логирования для чувствительных данных, либо применить обфускацию конфиденциальных. Также советую избегать явного хранения секретов в коде.

Пример:

Обратим внимание на схему хранения данных пользователей:

			CREATE TABLE users (
    id INTEGER PRIMARY KEY DEFAULT nextval('users_id_sequence'),
    login TEXT UNIQUE,
    password TEXT,
    is_admin BOOLEAN,
);
		

В текущем коде можно заметить, что пароли хранятся в базе данных в поле типа TEXT в открытом виде. Это крайне небезопасно, так как любой, кто имеет доступ к базе данных, может получить учетные записи. Для безопасности следует хранить не сами пароли, а их хэши.

Для этого рекомендую использовать криптографическую хэш-функцию (Argon2 или её модификацию Argon2id). Этот алгоритм обеспечит надежное и безопасное хранение паролей.

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

  1. Для работы с алгоритмом Argon2 следует использовать эту библиотеку.
  2. При регистрации пользователя записывать в базу данных только хэш пароля, а не сам пароль. Это выглядит следующим образом:
			func (a *App) signupHandler(c echo.Context) error {
 password := c.FormValue("password")

 // Дополнительная логика приложения
 hash, _ := argon2id.CreateHash(password, argon2id.DefaultParams)
 const query = `
  INSERT INTO users (username, password, is_admin) 
  VALUES (?, ?, ?) 
  RETURNING id
 `
 row := a.db.QueryRow(query, username, hash, isAdmin)
 // .. 
}

		

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

			func (a *App) loginHandler(c echo.Context) error {
 // ..
 password := c.FormValue("password")
 // Дополнительная логика
 row := a.db.QueryRow("SELECT id, password FROM users WHERE username = ?" username)
 var (
  userID int
  hash string
 )
 // Читаем данные в переменных из row
 // …
 // Сравниваем пароль и хэш-фукнцию, которая была записана при регистрации в БД
 match, err := argon2id.ComparePasswordAndHash(password, hash)
 if err != nil || !match {
  return c.String(http.StatusForbidden, "invalid credentails")
 }
 // ..
}

		

Шаг 5. Защита от SQL-инъекций: используйте подготовленные запросы

Проблема:

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

Что делать:

Для защиты от SQL-инъекций важно всегда использовать подготовленные выражения. Это гарантирует, что пользовательский ввод будет экранирован и не интерпретируется как часть SQL-запроса. Вместо того, чтобы вручную собирать запросы, всегда передавайте параметры через символы ? или $1, в зависимости от используемой базы данных. Советую никогда не добавлять данные напрямую в строку SQL-запроса, поскольку это открывает возможность для SQL-инъекций.

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

Пример:

			func (a *App) loginHandler(c echo.Context) error {
   username := c.FormValue("username")
   password := c.FormValue("password")

   row := a.db.QueryRow("SELECT id FROM users WHERE username = " + username + "AND password = " + password)
   var userID int
   if err := row.Scan(&userID); err != nil {
       return c.String(http.StatusUnauthorized, "invalid credentails")
   }

   return c.Redirect(http.StatusSeeOther, "/")
}

		

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

			func (a *App) loginHandler(c echo.Context) error {
 //..

 const query = `
  SELECT id 
  FROM users 
  WHERE 
   username = ? AND 
   password = ?
 `

 row := a.db.QueryRow(query, username, password)
 // ..
}

		

Если вы работаете с более сложными запросами в Go, то можно использовать библиотеку squirrel, которая помогает безопасно строить запросы и избегать ошибок при работе с SQL.

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

Кроме того, хочу обратить внимание на еще одну важную практику, которая часто упускается — использование инструмента golangci-lint. На данный момент это стандарт для разработчиков Go, хотя иногда можно встретить проекты, где инструмент не используется. Он позволяет автоматически находить уязвимости, ошибки и антипаттерны в коде на ранних стадиях разработки. В состав golangci-lint входит статический анализатор gosec, который ориентирован на поиск проблем безопасности. Использование инструмента помогает существенно снизить риски ошибок в продакшене. Для других популярных языков программирования также существуют аналогичные инструменты, которые стоит учитывать при разработке.

Перспективные технологии и подходы в защите backend

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

Zero Trust Architecture (ZTA) или Zero Trust Policy (ZTP)

Основывается на принципе «никому не доверять». В рамках этого подхода ни один запрос не считается доверенным по умолчанию, даже если он поступает из внутренней сети. Это означает, что сервисы, принадлежащие одной команде, не могут автоматически доверять друг другу. Для взаимодействия между ними необходимо запрашивать доступы, которыми можно централизованно управлять, что повышает уровень контроля и безопасности.

Гомоморфное шифрование (Homomorphic Encryption)

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

DevSecOps

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

Навыки backend-разработчика, которые защитят его от уязвимостей

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

В первую очередь — глубокое понимание принципов безопасности. Это включает в себя знание распространённых уязвимостей — SQL-инъекции, XSS, CSRF, SSRF и другие угрозы из списка OWASP. Важно также осознавать принципы минимизации привилегий и безопасного проектирования.

Неотъемлемая часть работы — практика безопасного программирования. Разработчик должен уметь работать с механизмами аутентификации и авторизации — OAuth 2.0, OpenID Connect и JWT, а также использовать защищённые соединения (TLS/HTTPS) и шифрование (AES, RSA).

Отдельного внимания заслуживает безопасность API. Инженер должен понимать, как проектировать защищённые интерфейсы, применять ограничения на частоту запросов, валидацию входных данных и проверку токенов. Кроме того, важно уметь работать с инструментами безопасности — SAST (SonarQube, Checkmarx), DAST (OWASP ZAP, Burp Suite) и SCA (Snyk, Dependabot).

Разбираясь в инфраструктуре и безопасности развертывания, разработчику необходимо уверенно управлять контейнерами (Docker, Kubernetes), работать с облачными сервисами (AWS, Azure, GCP) и использовать встроенные механизмы защиты. Также важно понимать принципы мониторинга и реагирования на инциденты: анализировать логи, настраивать системы оповещения и оперативно реагировать на угрозы.

Для повышения уровня безопасности команде пригодится не только обладать знаниями, но и регулярно их обновлять. Начать можно с изучения рейтинга OWASP Top 10, в котором собраны наиболее актуальные угрозы. Однако теория без практики бессмысленна, поэтому стоит использовать платформы для тренировки — Hack The Box, PortSwigger Academy и PentesterLab.

Внедрение DevSecOps-подходов значительно повышает защищённость системы. Автоматизированные инструменты — статический анализ кода (SAST), динамическое тестирование (DAST) и анализ зависимостей (SCA), должны быть интегрированы в процесс CI/CD.

Дополнительно можно организовать внутренние соревнования в формате Capture The Flag (CTF), чтобы вовлечь команду в процесс, и повысить уровень её осведомлённости. Если этот формат вам незнаком, можно изучить примеры, например, соревнования, проводимые Google — Google CTF.

Периодические анонимные проверки на уязвимости — тесты на фишинг или моделирование атак через подмену точек доступа, помогут выявить слабые места и укрепить защиту.

На мой взгляд, самое главное — создать в команде культуру безопасности. Регулярные обсуждения, обмен знаниями и поощрение сотрудников за выявленные уязвимости помогут не только снизить риски, но и сделать безопасность естественной частью рабочего процесса.

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

Полезные материалы по статье

Следите за новыми постами
Следите за новыми постами по любимым темам
3К открытий11К показов