Команда Go for Devs подготовила перевод статьи о новом подходе к защите Go-приложений от CSRF/CORF-атак. Автор разбирает, как связка TLS 1.3, SameSite cookies и http.CrossOriginProtection из стандартной библиотеки позволяют отказаться от токенов — но только если соблюдены важные условия. Насколько безопасен такой подход? Разбираемся.
Go 1.25 представил новый middleware http.CrossOriginProtection в стандартной библиотеке — и это заставило меня задуматься:
Неужели мы наконец пришли к тому моменту, когда CSRF-атаки можно предотвращать без токен-проверок (вроде double-submit cookies)? Можно ли теперь строить безопасные веб-приложения без сторонних пакетов вроде justinas/nosurf или gorilla/csrf?
И, кажется, теперь ответ может быть осторожным «да» — если соблюсти несколько важных условий.
Если хочешь пропустить объяснения и сразу посмотреть эти условия, нажми здесь.
http.CrossOriginProtection middleware
Новый middleware http.CrossOriginProtection работает так: он проверяет значения заголовков Sec-Fetch-Site и Origin, чтобы определить, откуда пришёл запрос. Он автоматически отклоняет любые «небезопасные» запросы, если они не с того же origin, и отправляет клиенту ответ 403 Forbidden.
У http.CrossOriginProtection есть свои ограничения, о которых мы поговорим чуть позже, но он надёжен, прост в использовании и стал отличным дополнением к стандартной библиотеке.
Как это работает
Современные браузеры автоматически добавляют заголовок
Sec-Fetch-Siteко всем запросам. Этот заголовок указывает, как соотносится origin страницы, которая отправляет запрос, и origin страницы, к которой обращаются. Два origin считаются одинаковыми, если полностью совпадают схема, хостнейм и порт (если он указан). В таком случае браузер добавляет в запрос заголовокSec-Fetch-Site: same-origin. Если origin не совпадают, браузер ставит иное значение, и тогдаhttp.CrossOriginProtectionотклонит запрос.Если заголовка
Sec-Fetch-Siteнет,http.CrossOriginProtectionобращается к заголовкуOrigin. Он просто сравниваетOriginиHost— если они различаются, значит запрос пришёл не с того же origin, и он будет отклонён.Если ни
Sec-Fetch-Site, ниOriginотсутствуют, middleware предполагает, что запрос пришёл не из браузера, и всегда разрешает его.Описанные проверки выполняются только для запросов с небезопасными HTTP-методами (
POST,PUTи т.п.). Запросы с безопасными методами (GET,OPTIONSи т.д.) всегда пропускаются.Если хочешь глубже разобраться в том, как проектировали
http.CrossOriginProtectionи почему приняли именно такие решения, обязательно почитай оригинальный пропозал Филиппо Вальсорды — он отлично написан.
В самом простом случае использовать его можно так:
File: main.go
package main
import (
"fmt"
"log/slog"
"net/http"
"os"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", home)
slog.Info("starting server on :4000")
// Wrap the mux with the http.NewCrossOriginProtection middleware.
err := http.ListenAndServe(":4000", http.NewCrossOriginProtection().Handler(mux))
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
}
func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello!")
Если нужно, поведение http.CrossOriginProtection можно настроить. Среди доступных параметров — добавление доверенных origin (которые могут отправлять cross-origin запросы) и использование собственного обработчика для отклонённых запросов вместо стандартного ответа 403 Forbidden.
Когда мне требовалась кастомизация, я использовал примерно такой подход:
File: main.go
package main
import (
"fmt"
"log/slog"
"net/http"
"os"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", home)
slog.Info("starting server on :4000")
err := http.ListenAndServe(":4000", preventCSRF(mux))
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
}
func preventCSRF(next http.Handler) http.Handler {
cop := http.NewCrossOriginProtection()
cop.AddTrustedOrigin("https://foo.example.com")
cop.SetDenyHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("CSRF check failed"))
}))
return cop.Handler(next)
}
func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello!")
}
Ограничения
Главное ограничение http.CrossOriginProtection — он эффективно блокирует только запросы из современных браузеров. Приложение всё равно остаётся уязвимым для CSRF-атак, если запросы придут из старых браузеров (обычно до 2020 года), которые не добавляют Sec-Fetch-Site или Origin.
Сейчас поддержка Sec-Fetch-Site составляет около 92%, а Origin — примерно 95%. Поэтому в общем случае полагаться только на http.CrossOriginProtection недостаточно для полноценной защиты от CSRF.
Важно также учитывать, что заголовок Sec-Fetch-Site отправляется только тогда, когда у приложения «доверенный origin» — фактически это значит, что в продакшене сайт должен работать по HTTPS (или использовать localhost при разработке), иначе http.CrossOriginProtection не сможет работать на полную мощность.
Нужно иметь в виду и то, что если заголовка Sec-Fetch-Site нет и включается проверка Origin против Host, то Host не содержит схему. Из-за этого http.CrossOriginProtection ошибочно пропустит cross-origin запросы с http://{host} на https://{host}, если заголовка Sec-Fetch-Site нет, но есть Origin. Чтобы снизить риск, лучше включить HTTP Strict Transport Security (HSTS).
Обязательное использование TLS 1.3
Разбираясь с этим, я задумался… Если вы всё равно планируете использовать HTTPS и сделать TLS 1.3 минимально поддерживаемой версией, можно ли быть уверенным, что все браузеры, поддерживающие TLS 1.3, также поддерживают хотя бы один из заголовков Sec-Fetch-Site или Origin?
Судя по данным совместимости на MDN и таблицам на Can I Use, ответ — «да» для (почти) всех основных браузеров.
Если вы требуете TLS 1.3:
Старые браузеры, не поддерживающие TLS 1.3, просто не смогут подключиться к вашему приложению.
А современные основные браузеры, которые TLS 1.3 поддерживают и могут подключиться, гарантировано имеют поддержку
Sec-Fetch-SiteилиOrigin— и значит,http.CrossOriginProtectionбудет работать корректно.
Единственное заметное исключение — Firefox версий 60–69 (2018–2019). Он не поддерживал Sec-Fetch-Site и не отправлял Origin для POST-запросов. Это значит, что http.CrossOriginProtection не сможет эффективно блокировать CSRF-запросы из этого браузера. По данным Can I Use, использование Firefox 60–69 сейчас составляет 0%, так что риск минимален — но где-то в мире наверняка ещё есть компьютеры, на которых он установлен.
Есть и другая оговорка: данные есть только по основным браузерам — Chrome/Chromium, Firefox, Edge, Safari, Opera и Internet Explorer. Но существуют и другие браузеры. Большинство из них — форки Chromium или Firefox, поэтому, скорее всего, всё будет нормально, но гарантировать это нельзя, и оценить риск сложно.
Таким образом, если вы используете HTTPS и требуете TLS 1.3, это огромный шаг вперёд, чтобы http.CrossOriginProtection работал максимально эффективно. Тем не менее остаётся небольшой риск, связанный с Firefox 60–69 и неосновными браузерами, поэтому возможно, вы захотите добавить защиту «на всякий случай» — например, использовать куки с флагом SameSite.
Мы поговорим о SameSite чуть позже, но сначала нужно сделать небольшое отступление и обсудить разницу между терминами origin и site.
Cross-site vs cross-origin
В мире веб-спецификаций и браузеров понятия cross-site и cross-origin похожи, но всё же отличаются — и в вопросах безопасности важно понимать разницу и формулировать её точно.
Кратко объясню.
Два сайта имеют один origin, если у них полностью совпадают схема, хостнейм и порт (если он указан). Поэтому https://example.com и https://www.example.com не являются одним origin — хостнеймы (example.com и www.example.com) разные. Запрос между ними будет cross-origin.
Два сайта считаются same-site, если у них совпадают схема и регистрируемый домен (registerable domain).
Регистрируемый домен — это часть хостнейма прямо перед (и включая) эффективный TLD. Несколько примеров:
Для
https://www.google.com/TLD —com, а регистрируемый домен —google.com.Для
https://login.mail.ucla.eduTLD —edu, регистрируемый домен —ucla.edu.Для
https://www.gov.ukTLD —gov.uk, регистрируемый домен —www.gov.uk.Полный список эффективных TLD можно посмотреть здесь.
Таким образом, https://example.com, https://www.example.com и https://login.admin.example.com — это same-site, потому что у них совпадают схема (https) и регистрируемый домен (example.com). Запросы между ними не считаются cross-site, но будут cross-origin.
Примечание: в некоторых версиях браузеров используется иное определение same-site, при котором совпадение схемы не требуется — достаточно одного и того же регистрируемого домена. В таких браузерах
https://admin.example.comиhttp://blog.example.comтоже будут считаться same-site.Сегодня это обычно называют schemaless same-site, но в старых версиях браузеров или документации это могли обозначать просто как same-site.
Итак, к каким выводам я подвожу?
-
Go-middleware
http.CrossOriginProtectionдействительно названа точно и по делу. Она блокирует cross-origin запросы. То есть работает строже, чем если бы блокировала только cross-site: она останавливает запросы и от других origin внутри одного и того же site (регистрируемого домена).Это полезно, потому что предотвращает ситуацию, когда ваш кривоватый WordPress-блог, который вы не обновляли лет десять и который работает на
https://blog.example.com, взламывают и используют для проведения атаки подделки запросов на ваш важный сайтhttps://admin.example.com. -
Когда большинство людей — и я в том числе — в быту говорят «CSRF-атака», речь чаще всего идёт именно о cross-origin request forgery, а не просто о cross-site. Обидно, что именно CSRF стало общепринятой аббревиатурой для этого семейства атак, потому что куда точнее и логичнее было бы говорить CORF. Но что поделать — мир у нас такой.
В оставшейся части текста я буду использовать термин CORF, когда речь идёт именно о нём.
SameSite cookies
Атрибут SameSite для куки в целом поддерживается браузерами с 2017 года, а в Go — с версии 1.11. Если установить для куки значение SameSite=Lax или SameSite=Strict, она будет отправляться только в запросах к тому же сайту, который её установил. Это предотвращает cross-site request forgery, но не защищает от cross-origin атак внутри одного и того же site.
Хорошая новость в том, что все основные браузеры, поддерживающие TLS 1.3, также полностью поддерживают SameSite, без исключений, которые мне удалось найти. Поэтому если вы требуете TLS 1.3, можно быть уверенным, что все крупные браузеры, работающие с вашим приложением, будут корректно обрабатывать атрибут SameSite.
Это означает, что, используя SameSite=Lax или SameSite=Strict для своих куки, вы закрываете риск cross-site подделки запросов со стороны Firefox 60–69, о котором мы говорили ранее.
Собираем всё вместе
Если совместить использование HTTPS, требование TLS 1.3 как минимальной версии, корректное применение куки с SameSite=Lax или SameSite=Strict, а также middleware http.CrossOriginProtection в вашем приложении, то, насколько я могу судить, остаются всего две неустранённые угрозы CSRF/CORF со стороны основных браузеров:
CORF-атаки внутри одного site (то есть с другого поддомена того же регистрируемого домена) в Firefox 60–69.
CORF-атаки с HTTP-версии вашего origin из браузеров, которые не поддерживают
Sec-Fetch-Site.
Для первой угрозы: если у вас нет других сайтов под тем же регистрируемым доменом или вы уверены, что эти сайты безопасны и не скомпрометированы, то — учитывая крайне низкое использование Firefox 60–69 — вы, возможно, сочтёте этот риск приемлемым.
Для второй угрозы: если вы вообще не обслуживаете HTTP-версию вашего origin (включая редиректы), то переживать не о чем. Если же HTTP всё-таки доступен, то снизить риск можно, добавив заголовок HSTS в HTTPS-ответы.
В начале статьи я говорил, что отказ от токен-проверки CSRF может быть приемлемым при определённых условиях. Давайте теперь перечислим эти условия:
Ваше приложение использует HTTPS и требует TLS 1.3 как минимальную версию. Вы принимаете тот факт, что пользователи со старыми браузерами не смогут подключиться вовсе.
Вы придерживаетесь хорошей практики и никогда не меняете важное состояние приложения в ответ на запросы с безопасными методами —
GET,HEAD,OPTIONSилиTRACE.Вы используете
http.CrossOriginProtectionи куки сSameSite=LaxилиSameSite=Strict.SameSiteважны как общая защита, но в частности — для снижения риска CSRF-атак со стороны Firefox 60–69.Из-за оставшегося риска same-site CORF в Firefox 60–69 у вас либо нет других сайтов под тем же регистрируемым доменом, либо вы уверены, что они безопасны и не скомпрометированы.
У вашего приложения либо совсем нет HTTP-версии origin, либо вы добавляете заголовок HSTS в HTTPS-ответы.
И наконец, вы готовы принять труднооцениваемый риск CSRF/CORF-атак со стороны некрупных браузеров, которые поддерживают TLS 1.3, но не поддерживают
Origin,Sec-Fetch-SiteилиSameSitecookies. Существуют ли такие браузеры? Не знаю, и, вероятно, невозможно ответить на этот вопрос со 100% уверенностью. Поэтому вам нужно провести собственную оценку риска — и принимать его стоит лишь тогда, когда ваш сайт является малопривлекательной целью, а последствия успешной CSRF/CORF-атаки будут ограниченными и незначительными.
Русскоязычное Go сообщество

Друзья! Эту статью подготовила команда «Go for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Go. Подписывайтесь, чтобы быть в курсе и ничего не упустить!