24. Безопасность
«Безопасность — это не состояние, а процесс. Она строится не на одном замке, а на непрерывной цепи взаимосвязанных слоёв защиты».
— Брюс Шнайер
- Что такое хеширование (hashing)?
- В чем разница между bcrypt и sha?
- Что такое JWT (JSON Web Token)?
- Как работает OAuth 2.0?
- Как работает OpenID Connect?
- Что такое TLS?
- Что такое mTLS (mutual TLS)?
- Как работают сертификаты?
- Что такое CORS?
- Что такое CSRF?
- Что такое XSS?
- Что такое SQL-инъекция?
- Как предотвратить SQL-инъекции?
- Что такое command injection?
- Что такое path traversal?
- Что такое SSRF?
- Что такое авторизация (authorization) vs аутентификация (authentication)?
- Что такое RBAC?
- Что такое симметричное и асимметричное шифрование?
- Как работать с секретами в Go (переменные окружения, vault)?
- Что такое "man-in-the-middle" атака?
- Что такое DDoS и как защититься?
- Как работает HTTPS и почему это важно?
- Что такое "сессии" и как их безопасно реализовать?
- Что такое SAML?
- Что такое "Content Security Policy" (CSP)?
- Как работает
x/crypto/argon2? - Что такое "радужные таблицы" и как соль их предотвращает?
- Как работает
x/crypto/nacl/secretbox? - Что такое "удостоверяющий центр" (CA) и цепочка доверия?
- Что такое "perfect forward secrecy" (PFS)?
- Что такое
security.txtи для чего он нужен? - Как работает
crypto/tlsи как настроить безопасные cipher suites? - Что такое "HTTP Strict Transport Security" (HSTS)?
- Что такое "clickjacking" и как защититься?
- Что такое "темпоральные данные" и как их безопасно передавать?
- Как работает
gosecи как он помогает писать безопасный код? - Что такое "принцип наименьших привилегий" (PoLP)?
- Как хранить пароли в базе данных (best practices)?
- Что такое "zero trust" архитектура?
- Как работает
webhooksignature verification? - Что такое "CSRF токен" и как его реализовать?
- Как предотвратить "DoS" атаки с помощью rate limiting?
- Что такое "regexp injection" (ReDoS) и как ее избежать?
- Как безопасно парсить пользовательский ввод?
- Что такое "supply chain attack" и как Go модули помогают защититься?
- Как работает
go mod verifyиchecksum databaseдля защиты от подмены модулей? - Что такое "form tampering" защита?
- Как настроить "cookie security" флаги (
HttpOnly,Secure,SameSite)? - Что такое Post-Quantum TLS в Go 1.24?
- Что такое crypto/hpke в Go 1.26?
- Что такое crypto/mldsa и Post-Quantum подписи в Go 1.27?
Что такое хеширование (hashing)?
Краткий ответ (30–60 секунд)
Хеширование — это процесс преобразования входных данных произвольной длины в детерминированную строку фиксированного размера (хеш-сумму) с помощью односторонней математической функции. Ключевые свойства: однозначность по входу, лавинный эффект, необратимость и стойкость к коллизиям. В Go часто применяют пакеты crypto/sha256, hash/fnv и crypto/hmac.
Развёрнутый ответ
Хеш-функция H(x) обладает свойствами:
- Детерминированность — один и тот же вход всегда даёт одинаковый хеш.
- Быстрое вычисление — для любого входа.
- Стойкость к поиску прообраза — по
hвычислительно невозможно найтиx, такой чтоH(x) = h. - Стойкость к поиску второго прообраза — зная
x, нельзя найтиx' ≠ xс тем же хешем. - Стойкость к коллизиям — нельзя найти пару
x, x'с одинаковым хешем. - Лавинный эффект — малейшее изменение входа меняет хеш непредсказуемо и кардинально.
Пример на Go (SHA-256):
import "crypto/sha256"
h := sha256.Sum256([]byte("hello"))
fmt.Printf("%x", h) // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
Применение: контроль целостности данных, хранение паролей (в сочетании с солью и ключевыми функциями), цифровые подписи, построение хеш-таблиц, HMAC для аутентификации сообщений.
Под капотом (internals)
В Go crypto/sha256 реализует алгоритм SHA-256 из семейства SHA-2. Внутренне используется структура digest, которая накапливает данные блоками по 64 байта и выполняет 64 раунда преобразований с использованием битовых операций, сдвигов и таблицы констант. Функция Sum вызывает checkSum и копирует внутреннее состояние, чтобы не портить исходный дайджест. На платформах с аппаратной поддержкой (ARM64 SHA-256) используется ассемблерная оптимизация. Пакет hash/fnv (Fowler-Noll-Vo) — быстрая не криптографическая хеш-функция для map-ключей, реализована без блокировок.
Типичные ошибки кандидатов
- Использование неподходящей хеш-функции для паролей
SHA-256 спроектирован для скорости и не содержит соли, что делает его уязвимым к перебору и радужным таблицам. Пароли нужно хешировать черезbcryptилиargon2со встроенной солью и высокой вычислительной стоимостью. - Хранение хешей без случайной соли
Даже криптостойкая хеш-функция без соли позволяет атакующему мгновенно проверять пароли по готовым радужным таблицам. Каждый пароль обязан солироваться уникальным криптографически безопасным случайным значением. - Использование устаревших алгоритмов (MD5, SHA-1) в security contexts
MD5 и SHA-1 имеют известные коллизии и не должны применяться для цифровых подписей, сертификатов или паролей. Go всё ещё содержитcrypto/md5только для обратной совместимости, не для новых систем.
Возможные дополнительные вопросы интервьюера
В чём отличие криптографической хеш-функции от некриптографической?
Криптографическая гарантирует свойства стойкости к прообразу и коллизиям, лавинный эффект и непредсказуемость. Некриптографические (например, FNV) ориентированы исключительно на скорость и равномерность распределения для хеш-таблиц, без гарантий безопасности.
Почему HMAC использует двойное хеширование?
Конструкция HMAC (внутренний и внешний padding) предотвращает атаки удлинением сообщения и обеспечивает безопасность даже при частичной слабости базовой хеш-функции, доказуемо сводя стойкость к стойкости нижележащего хеша.
Что такое атака удлинением сообщения и как Go защищает?
Атака позволяет, зная H(key+msg) и длину key+msg, создать валидный H(key+msg+padding+evil) без знания ключа. SHA-256 уязвим, поэтому HMAC в Go (пакет crypto/hmac) использует H((key⊕opad) || H((key⊕ipad) || msg)), что полностью нейтрализует эту атаку.
В чем разница между bcrypt и sha?
Краткий ответ (30–60 секунд)
SHA (например, SHA-256) — это быстрая криптографическая хеш-функция общего назначения, оптимизированная под скорость. Bcrypt — адаптивная ключевая функция с встроенной солью и настраиваемой стоимостью вычислений, специально спроектированная для защищённого хранения паролей. Bcrypt делает перебор паролей вычислительно неподъёмным.
Развёрнутый ответ
- SHA‑256 выполняет один проход, миллионы операций в секунду — идеален для целостности данных, HMAC, цифровых подписей. Не имеет встроенной соли; её нужно добавлять вручную, что часто делают неправильно.
- Bcrypt основан на блочном шифре Blowfish внутри модифицированной схемы ключевого расписания. Параметр
cost(число раундов, экспоненциально растущая сложность) делает его адаптивным: удвоениеcostудваивает время вычисления. Соль генерируется автоматически и включается в итоговый хеш. Вывод bcrypt содержит идентификатор версии, стоимость, соль и хеш, что упрощает миграцию и верификацию.
Пример в Go:
import "golang.org/x/crypto/bcrypt"
password := []byte("supersecret")
hash, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) // cost=10
// hash = $2a$10$...
err = bcrypt.CompareHashAndPassword(hash, password)
Под капотом (internals)
В golang.org/x/crypto/bcrypt реализован алгоритм, описанный в оригинальной статье 1999 года. Функция GenerateFromPassword создаёт 16-байтовую случайную соль, затем инициализирует Blowfish-подобный шифр с 448-битным ключом, расширенным за счёт многократного шифрования магической строки "OrpheanBeholderScryDoubt". Число раундов равно 2^cost. В ходе расширения ключа используется состояние EksBlowfishSetup, что делает алгоритм чрезвычайно затратным по времени и памяти, устойчивым к GPU/FPGA атакам. Результат кодируется в base64 в формате $2a$cost$salt+hash.
Типичные ошибки кандидатов
- Использование SHA для паролей
SHA быстр и без соли, что позволяет атакующему перебирать миллиарды комбинаций в секунду на GPU. Любой парольный хеш обязан использовать адаптивную функцию с солью. - Выбор слишком низкого
cost
bcrypt.MinCost=4даёт ничтожно малое время вычисления. На современном железеcost=12или выше должен подбираться так, чтобы операция занимала ~250-500ms, балансируя безопасность и UX. - Ручная реализация соли поверх SHA
Самописные схемы часто допускают ошибки экранирования, повторного использования соли, слабый источник случайности. Bcrypt делает это правильно из коробки.
Возможные дополнительные вопросы интервьюера
Почему не хранить пароль просто через SHA‑256(salt+password)?
SHA‑256 спроектирован для скорости, что помогает атакующему в брутфорсе. Соль мешает радужным таблицам, но не замедляет одиночную попытку. Современные GPU делают миллиарды SHA‑256 в секунду, взламывая даже сложные пароли.
Когда предпочесть argon2id вместо bcrypt?
Argon2id (победитель PHC) дополнительно устойчив к атакам с использованием памяти (memory-hard), что делает бесполезными специализированные ASIC. Рекомендуется для новых проектов, Go предоставляет golang.org/x/crypto/argon2.
Как в Go сравнить хеш bcrypt с паролем за константное время?
bcrypt.CompareHashAndPassword внутри использует subtle.ConstantTimeCompare для финального сравнения хешей, предотвращая timing-атаки, и предварительно извлекает соль и стоимость из сохранённого хеша.
Что такое JWT (JSON Web Token)?
Краткий ответ (30–60 секунд)
JWT — компактный, самодостаточный токен, состоящий из трёх base64url-кодированных частей: заголовок, полезная нагрузка, подпись. Используется для передачи утверждений (claims) между сторонами, обычно как средство аутентификации/авторизации в распределённых системах. Go-библиотеки: golang-jwt/jwt/v5.
Развёрнутый ответ
Структура: header.payload.signature.
- Header: алгоритм (HS256, RS256, EdDSA), тип
JWT. - Payload: набор зарегистрированных (
iss,sub,exp,iat) и пользовательских claims. - Signature: HMAC или асимметричная подпись от
header.payload, обеспечивающая целостность и аутентичность.
Токены не шифруют данные по умолчанию (все видят payload), потому никогда не храните в них секреты без JWE. JWT часто применяют в связке с OAuth2.0/OpenID Connect как access или ID-токены.
Пример на Go (создание и верификация):
import "github.com/golang-jwt/jwt/v5"
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": "1234567890",
"exp": time.Now().Add(time.Hour).Unix(),
})
tokenString, _ := token.SignedString([]byte("secret"))
parsed, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte("secret"), nil
})
Под капотом (internals)
Библиотека golang-jwt парсит токен, разделяя по точкам; декодирует base64 без padding (RawURLEncoding). SigningMethod определяет функцию подписи и хеширования. HMAC использует crypto/hmac, RSA — crypto/rsa с PSS. Верификация Parse возвращает ошибку при истекшем exp, несовпадении подписи или кривом формате. Валидация времени учитывает clock skew (по умолчанию 0). Для производительности пулы байтовых буферов не используются — каждый вызов создаёт новую строку, что надо учитывать при высоких нагрузках.
Типичные ошибки кандидатов
- Хранение JWT в localStorage
JavaScript имеет доступ, XSS позволяет украсть токен. JWT следует хранить в HttpOnly, Secure, SameSite cookie, куда JS не дотянется. - Игнорирование проверки алгоритма "none"
Библиотекаgolang-jwtпо умолчанию считаетnoneнебезопасным и требует явное разрешениеjwt.UnsafeAllowNoneSignatureType, но другие библиотеки могут пропустить. Всегда контролируйте список допустимых алгоритмов черезValidMethods. - Слепое доверие claims без верификации подписи
Никогда не используйте payload до вызоваParse()с ключом. Безусловное чтениеtoken.Claimsдо проверки подписи — критическая уязвимость.
Возможные дополнительные вопросы интервьюера
Как реализовать отзыв токена в stateless архитектуре?
JWT сам по себе не отзываем; решения: ведение чёрного списка jti в Redis с коротким TTL, либо уменьшение времени жизни токена и использование refresh-токенов, которые хранятся на сервере и могут быть заблокированы.
Чем ID Token отличается от Access Token?
ID Token (всегда JWT) подтверждает факт аутентификации и содержит информацию о пользователе для клиента. Access Token предназначен для доступа к ресурсам, может быть opaque, не всегда JWT, и клиент не должен его интерпретировать.
Почему в Go при создании токена важен тип ключа для SignedString?
SignedString ожидает interface{}, но, например, для RS256 нужен *rsa.PrivateKey. Передача []byte приведёт к панике, компилятор не проверяет. Всегда сопоставляйте метод подписи с типом ключа и покрывайте тестами.
Как работает OAuth 2.0?
Краткий ответ (30–60 секунд)
OAuth 2.0 — это фреймворк авторизации, позволяющий приложению получать ограниченный доступ к ресурсам пользователя на стороннем сервисе без раскрытия его пароля. Работает через перенаправления и выдачу access-токенов с помощью определённых потоков (authorization code, client credentials и др.).
Развёрнутый ответ
Роли: Resource Owner (пользователь), Client (приложение), Authorization Server, Resource Server.
Основной поток Authorization Code (PKCE):
- Клиент перенаправляет пользователя на
/authorizeсервера авторизации сclient_id,redirect_uri,scope,code_challenge. - Пользователь логинится и даёт согласие.
- Сервер возвращает
codeнаredirect_uri. - Клиент обменивает
code+code_verifierна токен через POST на/token. - Получает
access_token(и опциональноrefresh_token) и использует для запросов к API.
В Go реализация часто сводится к интеграции с провайдерами через golang.org/x/oauth2. Пример:
conf := &oauth2.Config{
ClientID: "id",
ClientSecret: "secret",
Endpoint: google.Endpoint,
RedirectURL: "https://myapp/callback",
Scopes: []string{"profile"},
}
url := conf.AuthCodeURL("state", oauth2.SetAuthURLParam("code_challenge", challenge))
// после получения code:
token, err := conf.Exchange(ctx, code, oauth2.SetAuthURLParam("code_verifier", verifier))
client := conf.Client(ctx, token)
Под капотом (internals)
Пакет x/oauth2 предоставляет абстрактный TokenSource, автоматически обновляющий токены через refresh_token. Внутри Token — это структура, сохраняемая в кеш-хранилище, по умолчанию в памяти. При вызове Client(ctx, token) создаётся *http.Client, который перехватывает запросы и добавляет заголовок Authorization: Bearer <access_token>; если токен истёк, синхронно обновляется с mutex, что гарантирует, что даже при конкурентных запросах истекающий токен обновится один раз. Exchange использует context для таймаутов. PKCE добавляется через опции: библиотека не генерирует code challenge автоматически — программист должен позаботиться.
Типичные ошибки кандидатов
- Отсутствие проверки
stateпараметра
Без проверки возможна CSRF-атака, заставляющая жертву авторизовать аккаунт злоумышленника. Всегда генерируйте криптографически случайныйstate, храните в сессии и сверяйте при callback. - Использование implicit flow вместо PKCE
Implicit flow возвращает токен в URL-фрагменте, уязвим к утечке через реферер и историю браузера. Для SPA и мобильных приложений обязательно использовать Authorization Code + PKCE. - Хранение
client_secretв публичном клиенте
В native/SPA приложенияхclient_secretне может быть надёжно сохранён. PKCE позволяет использовать клиента без секрета, что делает ненужным его встраивание в публичный код.
Возможные дополнительные вопросы интервьюера
Чем отличается OAuth 2.0 от OAuth 1.0a?
OAuth 1.0 требовал подписи каждого запроса криптографическими ключами, был сложен в реализации. OAuth 2.0 использует Bearer токены и TLS для безопасности, что проще, но требует жёсткого соблюдения HTTPS.
Как реализовать ротацию refresh-токенов в Go?
При каждом обмене refresh-токена выдавать новый refresh-токен и инвалидировать старый (хранить семейство в БД). В x/oauth2 можно перехватить токен через кастомный TokenSource и реализовать логику сохранения.
Зачем нужен aud (audience) claim в JWT access-токенах?
aud указывает, для какого ресурса предназначен токен. Resource Server обязан проверять aud, чтобы предотвратить использование токена, выданного для другого API, в своих целях.
Как работает OpenID Connect?
Краткий ответ (30–60 секунд)
OpenID Connect (OIDC) — это слой аутентификации поверх OAuth 2.0. Он стандартизирует получение идентичности пользователя через ID-токен (JWT) и предоставляет эндпоинт /userinfo для получения профиля. Даёт ответ на вопрос «кто ты?», тогда как OAuth 2.0 отвечает на «что ты можешь?».
Развёрнутый ответ
OIDC расширяет OAuth 2.0 дополнительным openid scope. В ответе на /token клиент получает id_token наряду с access_token. ID-токен содержит обязательные claims: iss, sub, aud, exp, iat. Клиент проверяет подпись (с помощью JWKS эндпоинта), iss, aud (должен совпадать с client_id) и время жизни. Дополнительно может запросить /userinfo с Bearer токеном, получая стандартные claims (name, email, picture).
В Go реализация часто с помощью dexidp/dex (сервер) или клиентом через coreos/go-oidc/v3:
provider, _ := oidc.NewProvider(ctx, "https://accounts.google.com")
config := oidc.Config{
ClientID: "myclient",
}
verifier := provider.Verifier(&config)
idToken, _ := verifier.Verify(ctx, rawIDToken)
var claims struct { Email string; Sub string }
idToken.Claims(&claims)
Под капотом (internals)
go-oidc реализует Provider, который извлекает метаданные из .well-known/openid-configuration, загружает публичные ключи с jwks_uri, кеширует их с учётом Cache-Control и Expires. Verifier выполняет все обязательные проверки: подпись (с использованием RS256/ES256), iss, aud, exp, iat с допуском clock skew (1 минута). Поддерживается ротация ключей: если ключ не найден, происходит рефетч JWKS. Ключи кешируются, обновление происходит атомарно с sync.RWMutex. Verify возвращает *IDToken, который лениво парсит claims с помощью encoding/json и проверяет nonce при наличии.
Типичные ошибки кандидатов
- Проверка только подписи, но не
audиiss
Если не проверитьaudна соответствиеclient_id, токен, выданный для другого приложения, будет принят как свой. OIDC библиотеки обычно проверяют, но при ручной реализации это пропускают. - Использование ID-токена для авторизации на ресурсном сервере
ID-токен подтверждает личность, но не содержит прав доступа (scopes). Ресурсный сервер должен принимать только access-токен, не ID-токен. - Парсинг ID-токена на фронтенде без верификации
Токен можно декодировать для отображения, но доверять claims нельзя до валидации на бекенде. Веб-клиенту достаточно сессионной cookie, а не ручной работы с токеном.
Возможные дополнительные вопросы интервьюера
Как обработать сценарий, когда провайдер ротирует ключи подписи в процессе верификации?
go-oidc автоматически перезагружает JWKS при ошибке верификации, но есть риск временного отказа. Лучшая практика — кешировать ключи с запасом и при 5xx ошибках быстро переключиться на резервный набор ключей с retry.
Для чего нужен nonce в OIDC?
nonce связывает запрос аутентификации с ID-токеном, предотвращая replay-атаки. Клиент генерирует случайный nonce, отправляет в запросе, и затем проверяет его наличие в полученном токене.
Как интегрировать OIDC с собственной RBAC?
Из ID-токена извлекается sub как уникальный идентификатор пользователя, затем из БД или токена (если включены группы) загружаются роли. Access-токен также может содержать группы, что снижает число запросов к БД.
Что такое TLS?
Краткий ответ (30–60 секунд)
TLS (Transport Layer Security) — криптографический протокол, обеспечивающий конфиденциальность, целостность и аутентификацию данных в сети. Использует асимметричное шифрование для согласования сессионных ключей, симметричное — для шифрования трафика и цифровые сертификаты X.509 для проверки сторон. Текущая версия TLS 1.3 (RFC 8446), Go полностью поддерживает через crypto/tls.
Развёрнутый ответ
Типичный TLS handshake (1.3):
- Client Hello: предлагает версии, cipher suites, ключ для DH.
- Server Hello: выбирает параметры, отправляет сертификат и свой ключ DH, завершает своей Finished.
- Клиент проверяет сертификат, вычисляет общий секрет, отправляет Finished.
Всё это за 1-RTT (в 1.3), далее данные шифруются AEAD (AES-GCM/ChaCha20-Poly1305).
В Go создание TLS сервера:
cert, _ := tls.LoadX509KeyPair("cert.pem", "key.pem")
cfg := &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS13}
ln, _ := tls.Listen("tcp", ":443", cfg)
flowchart TD
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
CH["1. ClientHello: список cipher suites + Key Share (X25519/ML-KEM)"]:::entry --> SH["2. ServerHello: выбор cipher + Server Key Share"]:::process
SH --> EE["3. Зашифрованные параметры: EncryptedExtensions"]:::accent
EE --> Cert["4. Сертификат сервера X.509 + CertificateVerify"]:::process
Cert --> FinS["5. Server Finished: сервер готов к передаче"]:::accent
FinS --> FinC["6. Client Finished: проверка подписи + HKDF derivation"]:::process
FinC --> Data["7. Установлен 1-RTT защищённый канал: передача Application Data (AEAD)"]:::success
Под капотом (internals)
crypto/tls в Go написан с нуля на чистом Go (кроме оптимизаций ASM для шифров). Никаких C-библиотек. Conn реализует net.Conn, внутри — буферизированный reader/writer, разделённый на записи Record. Handshake в 1.3 стартует с clientHelloMsg, сервер отвечает serverHelloMsg, encryptedExtensions, certificate, certificateVerify, finished. Ключи вычисляются через HKDF на основе эфемерного ECDHE (X25519 или P256). Проверка сертификатов идёт через x509.Certificate.Verify, которое строит цепочку до корневого сертификата, проверяет сроки и DNS/ip SAN. Чтение и запись используют crypto/aes, crypto/cipher и poly1305. В Go 1.22 по умолчанию отключены слабые cipher suites, включена поддержка 0-RTT (ранние данные).
Типичные ошибки кандидатов
- Установка
InsecureSkipVerify: true
Позволяет любому самоподписанному сертификату пройти проверку, фактически отключает аутентификацию сервера. Допустимо только для тестов или доверенных кастомных CA черезRootCAs. - Использование устаревших версий TLS (1.0, 1.1)
Они содержат уязвимости (POODLE, BEAST). В Go нужно явно выставитьMinVersion: tls.VersionTLS12, а лучшеtls.VersionTLS13. - Жёсткое кодирование cipher suites
Пытаться вручную задавать список шифров вместо использования безопасных дефолтов. В 1.3 cipher suites управляются автоматически, и Go игнорирует пользовательские значения для 1.3.
Возможные дополнительные вопросы интервьюера
Что такое TLS fingerprinting и как Go может помочь избежать выделения?
Клиентские параметры (cipher suites, расширения) создают отпечаток, позволяющий идентифицировать инструмент. Используя tls.Config с тонкой настройкой или библиотеку utls, можно имитировать популярные браузеры и избежать блокировок.
Как в Go реализовать взаимный TLS (mTLS)?
Добавить в tls.Config.ClientAuth = tls.RequireAndVerifyClientCert и указать ClientCAs — пул доверенных клиентских CA. Клиенты предоставляют свой сертификат при соединении.
Объясните работу сессионных ticket'ов и как они отключаются в Go.
Сессионные ticket'ы позволяют клиенту возобновить сессию без хранения серверного состояния. В Go SessionTicketsDisabled: true отключает их. Без ticket'ов используется server-side сессионный кеш, что при множестве серверов требует распределённого хранилища.
Что такое mTLS (mutual TLS)?
Краткий ответ (30–60 секунд)
mTLS — расширение TLS, при котором обе стороны (клиент и сервер) предъявляют и проверяют сертификаты друг друга. Обеспечивает двухстороннюю аутентификацию на транспортном уровне, часто применяется в service mesh, Zero Trust архитектурах и межсервисном взаимодействии.
Развёрнутый ответ
В обычном TLS сервер предоставляет сертификат, клиент опционально. В mTLS сервер запрашивает клиентский сертификат в CertificateRequest во время handshake. Клиент отправляет свой сертификат и подпись (CertificateVerify), подтверждая владение закрытым ключом. Оба конца проверяют цепочку доверия до известных CA.
Пример настройки Go mTLS сервера:
caCert, _ := os.ReadFile("ca.pem")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caCertPool,
MinVersion: tls.VersionTLS13,
}
ln, _ := tls.Listen("tcp", ":8443", tlsConfig)
Клиент:
cert, _ := tls.LoadX509KeyPair("client.pem", "client-key.pem")
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
}
conn, _ := tls.Dial("tcp", "server:8443", tlsConfig)
Под капотом (internals)
В crypto/tls серверная часть при ClientAuth >= RequestClientCert отправляет certificateRequestMsg после своего certificate. Клиент в ответ шлёт certificateMsg и certificateVerifyMsg. Проверка клиентского сертификата осуществляется вызовом cert.Verify(opts), где opts.Roots = ClientCAs; если пул пуст — проверка пропускается (но при RequireAndVerifyClientCert без пула произойдёт ошибка). Также проверяется срок действия и ключевое использование ExtKeyUsageClientAuth. Глубинная проверка цепочки использует алгоритм построения, аналогичный x509.Certificate.Verify. После верификации сервер извлекает клиентский сертификат из ConnectionState.PeerCertificates.
Типичные ошибки кандидатов
- Неправильная настройка
ClientAuth
tls.RequestClientCertтолько запрашивает сертификат, но не требует его и не валидирует. Для полноценного mTLS нужноtls.RequireAndVerifyClientCert. - Отсутствие ограничения на
ExtKeyUsageв CA
Если CA не задаётExtKeyUsageтолько для клиентской/серверной аутентификации, сертификат для web-сервера может быть использован как клиентский. При выпуске указывайтеx509.ExtKeyUsageClientAuth. - Загрузка
ClientCAsкак системного пула вместо своего
По умолчаниюnilозначает системный пул, который часто не содержит ваш внутренний CA. Всегда явно задавайтеRootCAsиClientCAs.
Возможные дополнительные вопросы интервьюера
Как в mTLS реализовать ротацию сертификатов без обрыва соединений?
Сервер может использовать GetConfigForClient, который возвращает новый tls.Config с обновлённым сертификатом для каждого соединения, либо перезагружать tls.Config.Certificates с помощью sync/atomic при работающем сервере.
Почему в service mesh (Istio) mTLS работает даже без изменения кода приложения?
Sidecar-прокси (Envoy) перехватывает трафик и терминирует/инициирует mTLS. Приложение общается с localhost по HTTP, а прокси подменяет сертификаты, автоматически обеспечивая двухстороннюю аутентификацию.
Как в Go ограничить список допустимых клиентских сертификатов по Subject?
После установки соединения можно прочитать tls.Conn.ConnectionState().PeerCertificates, найти нужный сертификат и вручную проверить Subject.CommonName или SAN, но это дополнительный шаг после handshake, который нужно реализовать обёрткой над net.Listener.
Как работают сертификаты?
Краткий ответ (30–60 секунд)
X.509 сертификат — это цифровой документ, связывающий открытый ключ с субъектом (домен, организация) и подписанный удостоверяющим центром (CA). Проверка включает построение цепочки доверия до корневого самоподписанного сертификата, проверку подписей, срока действия и политик использования.
Развёрнутый ответ
Сертификат содержит: версию, серийный номер, алгоритм подписи, эмитента, срок действия, субъекта, публичный ключ, расширения (SAN, Key Usage, Extended Key Usage), и подпись CA. Процесс верификации:
- Построение цепочки от конечного сертификата до корня через промежуточные.
- Проверка подписи каждого звена публичным ключом вышестоящего.
- Проверка срока действия всей цепочки.
- Проверка, что корень находится в доверенном хранилище.
- Проверка ограничений (name constraints, basic constraints CA:TRUE/FALSE).
- Соответствие имени (CN/SAN) запрашиваемому хосту.
В Go:
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(rootPEM)
opts := x509.VerifyOptions{
DNSName: "example.com",
Roots: pool,
}
_, err := cert.Verify(opts)
Под капотом (internals)
Пакет crypto/x509 парсит ASN.1 DER-структуру сертификата. Verify строит возможные цепочки с помощью buildChains, используя DFS по сертификатам из пула Intermediates и Roots. Проверяет IsCA, MaxPathLen, ключевые использования. Для DNSName сопоставляет с SAN или CN. При проверке времени использует time.Now() с возможностью задания CurrentTime. Подпись проверяется через CheckSignature с алгоритмом, зависящим от SignatureAlgorithm (ECDSA, RSA-PSS, Ed25519). Go также поддерживает revocation проверку через OCSP (crypto/ocsp) и CRL (crypto/x509/pkix), но по умолчанию не активирована — разработчик должен вызвать отдельно.
Типичные ошибки кандидатов
- Игнорирование SAN и проверки только CN
Согласно RFC 2818, CN обязан игнорироваться при наличии SAN. Если сертификат содержит SAN, а вы проверяете только CN, валидация может пройти неверно. Go вVerifyOpts.DNSNameавтоматически проверяет SAN, затем CN. - Загрузка только конечного сертификата без промежуточных
Сервер должен отдавать полную цепочку (или клиент должен иметь промежуточные в пуле). ИначеVerifyможет не суметь построить цепочку до корня, даже если корень доверен. - Неверное понимание параметра
InsecureSkipVerify
Он пропускает проверку сертификата полностью, не то же самое, что добавить свой CA. Правильно — добавить свой CA вRootCAs.
Возможные дополнительные вопросы интервьюера
Как работает HPKP (Public Key Pinning) и почему он устарел?
HPKP позволял серверу указывать пины публичных ключей, но при ошибке настройки приводил к невосстановимому DoS для пользователей. В Go пиннинг реализуется вручную через колбек VerifyPeerCertificate.
Что такое ACME и как автоматизировать выпуск сертификатов в Go?
ACME (RFC 8555) — протокол для автоматического выпуска сертификатов (Let’s Encrypt). Go-пакет golang.org/x/crypto/acme/autocert позволяет встроить ACME-клиент прямо в tls.Config.GetCertificate, обеспечивая автоматическое получение и обновление.
Как проверить отзыв сертификата в Go?
Нужно либо выполнить OCSP запрос (ocsp.ParseResponse), либо загрузить CRL и проверить серийный номер. В продакшене обычно применяют OCSP Stapling, где сервер прикрепляет OCSP-ответ к handshake; в crypto/tls это настраивается через tls.Config.GetCertificate и стейплинг происходит автоматически при наличии.
Что такое CORS?
Краткий ответ (30–60 секунд)
Cross-Origin Resource Sharing — механизм, контролирующий доступ веб-страниц к ресурсам другого источника (origin = схема+хост+порт). Сервер с помощью HTTP-заголовков (Access-Control-Allow-Origin, Access-Control-Allow-Methods) явно разрешает кросс-доменные запросы, предотвращая нежелательный доступ.
Развёрнутый ответ
Браузер ограничивает same-origin policy: скрипт с origin A не может прочитать ответ от origin B без специального разрешения. CORS добавляет preflight-запрос (OPTIONS) для «сложных» запросов, сервер отвечает заголовками:
Access-Control-Allow-Origin: разрешённый origin (или*). С*нельзя использовать credentials.Access-Control-Allow-Methods: разрешённые методы.Access-Control-Allow-Headers: разрешённые пользовательские заголовки.Access-Control-Allow-Credentials:trueдля передачи cookies/HTTP-аутентификации.
В Go обычно используют middleware:
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "https://trusted.com")
if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
w.Header().Set("Access-Control-Allow-Headers", "Authorization")
return
}
next.ServeHTTP(w, r)
})
}
Под капотом (internals)
Go стандартная библиотека не включает CORS middleware. Браузеры сами обрабатывают заголовки; сервер должен корректно отвечать на OPTIONS и на обычные запросы включать Access-Control-Allow-Origin. В Go при использовании стандартного net/http нужно обрабатывать preflight вручную. Многие библиотеки (например, rs/cors) создают обработчик, который проверяет Origin, сопоставляет с разрешённым списком и динамически выставляет заголовки. Хитрость: при запросе с credentials: 'include' нельзя ставить wildcard-ориджин, нужно зеркально отражать конкретный Origin из запроса, если он доверен, иначе браузер отклонит ответ.
Типичные ошибки кандидатов
- Использование
*совместно сcredentials: 'include'
По спецификации это игнорируется браузером, и CORS не срабатывает. При работе с куками надо всегда указывать точный Origin и заголовокVary: Origin. - Забывают
Vary: Originпри динамическом Origin
Кеширующие прокси могут сохранить ответ с одним Origin и отдать его для другого, что нарушит безопасность.Vary: Originзаставляет кеши разделять ответы. - Разрешение всех методов и заголовков без необходимости (избыточные разрешения)
Access-Control-Allow-Methods: *не поддерживается; необходимо перечислять конкретные методы. Избыточные разрешения расширяют поверхность атаки.
Возможные дополнительные вопросы интервьюера
Как обрабатывать несколько доверенных origin в Go?
Читаем заголовок Origin, проверяем наличие в списке доверенных, и если есть — выставляем его же в Access-Control-Allow-Origin, иначе не добавляем заголовок (браузер сам отклонит).
Что такое preflight запрос и когда он отправляется?
Preflight (OPTIONS) отправляется для запросов, не являющихся простыми: методы кроме GET/HEAD/POST, пользовательские заголовки, Content-Type отличный от form-urlencoded/multipart/text/plain. Цель — проверить разрешения до отправки основного запроса.
Как отключить CORS полностью?
Никак — CORS — это механика браузера по ослаблению same-origin policy. Сервер не может сказать «не использовать CORS». Если сервер не шлёт нужных заголовков, браузер блокирует ответ.
Что такое CSRF?
Краткий ответ (30–60 секунд)
Cross-Site Request Forgery — атака, заставляющая аутентифицированного пользователя выполнить нежелательное действие на сайте, где он залогинен. Злоумышленник создаёт запрос от имени жертвы, используя сохранённые куки. Защита строится на использовании уникального секретного токена, привязанного к сессии, который браузер не может автоматически прикрепить для cross-site запросов.
Развёрнутый ответ
Классический сценарий: пользователь авторизован в банке. Злоумышленник размещает на своём сайте форму, отправляющую POST-запрос на перевод денег на /transfer с параметрами. Браузер автоматически прикрепит куки сессии, и сервер выполнит операцию. Основная защита — CSRF токен: сервер генерирует случайный токен для сессии, внедряет его в форму как скрытое поле (<input type="hidden" name="csrf_token" value="...">) и/или в заголовок (для JS-запросов). При POST/PUT/DELETE запросе токен должен совпадать с сохранённым на сервере.
Go реализация с использованием Gorilla CSRF или вручную:
// генерация токена и вставка в шаблон
csrfToken := csrf.Token(r)
// проверка в middleware
func CSRFMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "HEAD" {
token := r.PostFormValue("csrf_token")
if subtle.ConstantTimeCompare([]byte(token), []byte(sessionToken)) != 1 {
http.Error(w, "Forbidden", 403)
return
}
}
next.ServeHTTP(w, r)
})
}
Дополнительная защита: SameSite Cookie (Lax или Strict), проверка заголовка Origin/Referer, использование кастомных заголовков для AJAX (браузер запрещает устанавливать кастомные заголовки без CORS preflight).
Под капотом (internals)
В Go стандартной библиотеке CSRF не встроен. Реализация middleware генерирует токен с помощью crypto/rand, хранит его в серверной сессии (куки, Redis). При верификации крайне важно использовать crypto/subtle.ConstantTimeCompare, чтобы избежать timing-атаки. gorilla/csrf использует маскированный токен: комбинацию случайного токена и хеш от пользовательской сессии, что позволяет не хранить состояние на сервере (stateless CSRF). Токен помещается в заголовок X-CSRF-Token и в куку, а при проверке они сравниваются.
Типичные ошибки кандидатов
- Отправка CSRF токена в куках без дополнительной проверки
Токен в куках тоже будет отправлен автоматически браузером, атака не предотвращается. Токен должен передаваться способом, который злоумышленник не может подделать: тело запроса или нестандартный заголовок. - Использование GET-запросов для изменения состояния
GET запросы должны быть идемпотентны и не изменять данные. Изменяющими должны быть только POST/PUT/DELETE, что уменьшает риск CSRF через<img>или<script>. - Отсутствие проверки
Origin/Refererкак дополнительного слоя
Хотя эти заголовки можно подделать в некоторых браузерных условиях, разумная проверка даёт defence-in-depth. Отбрасывать запросы с несовпадающим Origin (кроме случаев снятия через политики) стоит.
Возможные дополнительные вопросы интервьюера
Как работает Double Submit Cookie паттерн?
Сервер устанавливает куку с случайным токеном, JavaScript читает её и добавляет в кастомный заголовок. Злоумышленник с другого origin не может прочитать куку и добавить заголовок из-за same-origin policy. Сервер сравнивает значение куки и заголовка. Минус — требует JavaScript и уязвим при поддоменных куках.
Почему SameSite Cookie помогает, но не является серебряной пулей?
SameSite=Lax предотвращает отправку кук для cross-site POST, но не защищает от top-level навигаций GET (менее опасно) и не поддерживается старыми браузерами. SameSite=Strict полностью блокирует, но может сломать кросс-сайт ссылки на аутентифицированные страницы.
Как реализовать CSRF защиту для API, использующего Bearer токены вместо кук?
Bearer токены добавляются через заголовок Authorization, который браузер не вставляет автоматически. Сама природа схемы (JS должен явно добавить токен) предотвращает CSRF. Достаточно убедиться, что токен не хранится в куках и не используется базовая аутентификация.
Что такое XSS?
Краткий ответ (30–60 секунд)
Cross-Site Scripting — уязвимость, позволяющая внедрить вредоносный скрипт в веб-страницу, просматриваемую жертвой. Злоумышленник может украсть сессию, перенаправить пользователя, подменить контент. Виды: Stored, Reflected, DOM-based. Защита — строгое контекстное экранирование вывода и Content Security Policy.
Развёрнутый ответ
- Stored XSS: вредоносный код сохраняется на сервере (комментарий) и выполняется у всех посетителей.
- Reflected XSS: ввод немедленно возвращается в ответе сервера (параметр в URL).
- DOM-based XSS: уязвимость на стороне клиента, когда JavaScript опасным образом обрабатывает данные из DOM (innerHTML, eval).
В Go для предотвращения используем html/template, который автоматически экранирует данные в зависимости от контекста (HTML, атрибуты, JS, CSS). Пример:
tmpl, _ := template.New("page").Parse("<h1>{{.Title}}</h1>")
data := struct{ Title string }{Title: "<script>alert(1)</script>"}
tmpl.Execute(w, data) // безопасно выведет <script>
Для JSON-ответов экранирование не требуется, но нужно избегать json.HTMLEscape. Для работы с сырым HTML используем template.HTML только для доверенных данных.
Под капотом (internals)
Пакет html/template анализирует шаблон и создаёт AST, вычисляя контекст (HTMLText, HTMLAttr, JS, CSS, URL). Внутри функции htmlEscaper, jsStrEscaper заменяют опасные символы. Контекст передаётся через состояние парсинга при обходе дерева. template.HTML отключает экранирование. Библиотека html/template автоматически экранирует и вложенные вызовы, если в шаблоне используется {{template "name"}}. Проверка типов происходит на этапе компиляции шаблона. template также защищает от XSS через экранирование в URL-ссылках: javascript: автоматически отбрасывается.
Типичные ошибки кандидатов
- Ручное формирование HTML через
fmt.Sprintfвместо шаблонов
При конкатенации строк отсутствует контекстное экранирование, злоумышленник может вставить произвольный HTML/JS. Всегда использоватьhtml/template. - Использование
template.HTMLдля пользовательского ввода
Типtemplate.HTMLговорит «это безопасный HTML», и шаблонизатор его не экранирует. Нельзя доверять такому вводу, всегда экранировать перед передачей. - Доверие данным из запроса в JavaScript-контексте
Даже если экранирован HTML, при вставке в<script>var name = "{{.Name}}";</script>необходима дополнительная JS-escape обработка. Вhtml/templateэто делается автоматически, но при ручной генерации легко ошибиться.
Возможные дополнительные вопросы интервьюера
Как Content Security Policy может предотвратить эксфильтрацию данных при успешном XSS?
CSP с default-src 'self' и запретом inline скриптов (script-src 'nonce-...') не позволит злоумышленнику загрузить чужой скрипт или выполнить inline-код, даже если удалось внедрить HTML-тег.
Чем отличается html/template от text/template для генерации HTML?
html/template знает о контексте и автоматически экранирует данные в зависимости от места (HTML, атрибут, JS, CSS). text/template этого не делает и абсолютно не должен использоваться для HTML-ответов.
Что такое Auto-Escaping в Go шаблонах и где он может подвести?
Auto-escaping защищает большинство случаев, но при передаче данных в JS-функции через onclick="myFunc('{{.}}')" экранирование применяется, однако при вставке в json+html вручную может понадобиться использование json.Marshal для защиты от XSS в JSON-контексте.
Что такое SQL-инъекция?
flowchart TD
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
OWASP["OWASP Top 10 в архитектуре Go-бэкенда"]:::entry
OWASP --> A1["A01: Broken Access Control"]:::error
A1 --> S1["Решение: RBAC/ABAC middleware, валидация subject ID на уровне БД"]:::success
OWASP --> A2["A02: Cryptographic Failures"]:::error
A2 --> S2["Решение: crypto/rand, TLS 1.3, Argon2id для паролей, AES-GCM/ChaCha20"]:::success
OWASP --> A3["A03: Injection (SQL / Command / Path)"]:::error
A3 --> S3["Решение: database/sql плейсхолдеры ($1), os.Root, отказ от exec в shell"]:::success
OWASP --> A5["A05: Security Misconfiguration"]:::warning
A5 --> S5["Решение: HttpOnly/Secure cookies, CSP заголовки, gosec в CI/CD"]:::process
OWASP --> A10["A10: SSRF (Server-Side Request Forgery)"]:::error
A10 --> S10["Решение: кастомный net.Dialer с блокировкой private/loopback IP (CIDR check)"]:::success
Краткий ответ (30–60 секунд)
SQL-инъекция — уязвимость, позволяющая злоумышленнику изменить предполагаемую структуру SQL-запроса через пользовательский ввод, что может привести к чтению, изменению или удалению данных, а иногда к выполнению команд ОС. Причина — конкатенация строк при построении запроса вместо параметризации.
Развёрнутый ответ
Пример уязвимого кода на Go:
id := r.URL.Query().Get("id")
row := db.QueryRow("SELECT name FROM users WHERE id = " + id)
Злоумышленник передаёт 1 OR 1=1 — получает все записи. Или 1; DROP TABLE users--. Лечение — параметризованные запросы (prepared statements):
row := db.QueryRow("SELECT name FROM users WHERE id = $1", id)
Драйвер базы данных (например, pgx или database/sql) отправляет параметр отдельно от запроса, предотвращая интерпретацию данных как код. Даже если в параметрах переданы спецсимволы, они экранируются на уровне протокола.
Под капотом (internals)
database/sql взят на вооружение драйвером (database/sql/driver). При использовании плейсхолдеров ($1, ?), драйвер применяет extended query protocol (PostgreSQL) или prepared statement API (MySQL), передавая SQL текст и параметры раздельно. В PostgreSQL pgx использует бинарный протокол, где параметры кодируются в отдельных полях, и SQL интерпретируется до подстановки. В Go db.QueryRow под капотом создаёт Stmt, который компилируется на сервере один раз, затем исполняется с параметрами. Для ручного построения сложных динамических частей (имена таблиц, столбцов) нужно применять строгую валидацию по белому списку, никогда не вставляя пользовательский ввод напрямую.
Типичные ошибки кандидатов
- Конкатенация строк для сборки WHERE условий
Даже с использованиемfmt.Sprintfи подготовленного выражения иногда кандидаты пишутstmt := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", name). Это сводит на нет параметризацию. Всегда используйте$1и передавайте параметры. - Ручное экранирование вместо параметризации
Попытки вручную экранировать кавычки черезstrings.ReplaceAll(name, "'", "''")ненадёжны, не учитывают особенности разных СУБД и многобайтовые последовательности. Параметризованный запрос — единственный верный подход. - Динамическая подстановка имён таблиц / полей без белого списка
Иногда необходимо менять сортировку или поле. Тогда надо проверять ввод по списку разрешённых значений (if column != "name" && column != "age" { return error }), иначе открывается инъекция второго порядка.
Возможные дополнительные вопросы интервьюера
Что такое Second Order SQL Injection?
Вредоносные данные сохраняются в БД без немедленного выполнения, затем позже другой запрос извлекает их и использует при построении динамического SQL. Защита та же: везде параметризация, даже при использовании данных из собственной базы.
Как в Go обезопасить LIKE запросы?
Параметр для LIKE также передаётся через placeholder, экранирование символов % и _ выполняется вручную перед передачей: escaped := strings.ReplaceAll(input, "%", "\\%") и т.д., после чего используется LIKE $1.
Почему использование ORM не гарантирует отсутствие SQL-инъекций?
ORM генерирует запросы, но при использовании «сырых» SQL или динамических частей без параметров уязвимости сохраняются. Например, GORM db.Raw("SELECT * FROM users WHERE name = ?", name) безопасен, но db.Raw("SELECT * FROM users WHERE name = '" + name + "'") — нет.
Как предотвратить SQL-инъекции?
Краткий ответ (30–60 секунд)
Полное исключение ручной сборки SQL-строки. Использовать параметризованные запросы (placeholders), prepared statements и ORM/Query Builder, которые делают это автоматически. Для динамических частей (имена таблиц/столбцов) — валидация по белому списку. Также полезна минимализация привилегий СУБД (только нужные права) и использование хранимых процедур.
Развёрнутый ответ
Надёжная защита требует подхода на нескольких уровнях:
-
Параметризация — закон:
Go stmt, _ := db.Prepare("INSERT INTO logs (msg, severity) VALUES ($1, $2)") stmt.Exec(userMsg, severity) -
Белый список для идентификаторов:
Go allowedColumns := map[string]bool{"id": true, "username": true, "email": true} if !allowedColumns[orderBy] { return errors.New("invalid column") } query := fmt.Sprintf("SELECT * FROM users ORDER BY %s", orderBy) -
Принцип наименьших привилегий для пользователя БД: приложение подключается под юзером, который не имеет прав на DDL, доступ только к необходимым таблицам.
- Экранирование для специфических случаев (LIKE-паттерны) происходит вручную после параметризации, а не вместо неё.
- WAF и мониторинг как дополнительный слой.
Под капотом (internals)
Параметризованный запрос через database/sql в Go отправляет SQL и параметры раздельно. Драйвер на уровне протокола (например, PostgreSQL wire protocol v3) использует сообщение Parse для подготовки, затем Bind с бинарными данными параметров. Сервер выполняет план запроса с уже подставленными параметрами как данными, а не SQL кодом. С точки зрения движка БД, SQL-текст разбирается и компилируется до подачи значений, инъекция невозможна структурно. Go дополнительно обеспечивает безопасное кодирование в драйвере, проверяя типы через database/sql/driver.ValueConverter.
Типичные ошибки кандидатов
- Использование
db.Query(fmt.Sprintf(...))с пользовательскими данными
Даже если внутри есть плейсхолдеры, построение самого запроса через Sprintf с данными открывает дыру. Правильно передавать параметры отдельно. - Избыточное ручное экранирование + параметризация, что приводит к двойному экранированию или искажению данных
Если драйвер получает уже экранированную строку, он добавит своё, запись окажется искажённой. Доверьтесь драйверу. - Предположение, что ORM делает всё безопасно, и игнорирование сырых SQL методов
Разработчик может использоватьdb.Exec("UPDATE ...")без параметров, считая, что ORM всё экранирует. Всегда проверяйте документацию и используйте только параметризованные вызовы.
Возможные дополнительные вопросы интервьюера
Можно ли полностью автоматически обнаруживать SQL-инъекции в коде?
Статический анализатор вроде gosec или golangci-lint с sqlclosecheck и rowserrcheck может найти конкатенацию в вызовах SQL, но не всегда. Полное доверие невозможно; необходим код-ревью и пентесты.
Как защитить приложение, если используется библиотека, генерирующая динамические запросы?
Если библиотека не даёт использовать параметризацию для каких-то частей — искать альтернативу или экранировать на стороне приложения с полным пониманием контекста. Лучшее решение — отказаться от неподдерживаемых библиотек.
Почему хранимая процедура с динамическим SQL тоже может быть уязвима?
Если внутри хранимой процедуры строится запрос через конкатенацию с параметром (EXECUTE IMMEDIATE без USING), уязвимость сохраняется. Процедуры также должны использовать параметризацию.
Что такое command injection?
Краткий ответ (30–60 секунд)
Command Injection — уязвимость, позволяющая злоумышленнику выполнить произвольные команды операционной системы на сервере через недостаточно очищенный ввод, переданный в функции запуска процессов (exec.Command, os/exec в Go). Возникает, когда пользовательские данные вставляются в командную строку без должной изоляции.
Развёрнутый ответ
Классический пример на Go:
ip := r.URL.Query().Get("ip")
cmd := exec.Command("sh", "-c", "ping -c 1 "+ip)
out, _ := cmd.CombinedOutput()
Злоумышленник передаёт 8.8.8.8; rm -rf /, и shell выполняет обе команды. Защита:
-
Никогда не использовать shell в
exec.Commandс пользовательскими данными. Вместо этого передавать аргументы напрямую:Go cmd := exec.Command("ping", "-c", "1", ip)В таком варианте
ip="8.8.8.8; rm -rf /"станет одним аргументом команды ping, и инъекция не сработает. -
Если shell действительно нужен, тщательно экранировать аргументы через
shellquoteили использоватьtext/templateдля генерации команд с проверкой. - Изоляция: выполнение в контейнере с минимальными правами, без лишних capability, read-only файловая система.
Под капотом (internals)
Пакет os/exec при вызове Command(name, arg...) не использует shell по умолчанию — каждая строка аргумента поступает напрямую в execve syscall. Только если name = "sh" и -c, то shell интерпретирует строку. syscall.Exec в Unix передаёт аргументы как массив, shell не участвует. В Windows используется CreateProcessW с отдельным аргументом cmdLine, но экранирование сложное. Поэтому первый принцип — избегать shell. Cmd также позволяет задавать SysProcAttr, Credential для смены пользователя, Chroot для дополнительной изоляции (на Linux).
Типичные ошибки кандидатов
- Использование
exec.Command("sh", "-c", fmt.Sprintf("ffmpeg -i %s ...", path))
Даже если path содержит спецсимволы, оболочка их интерпретирует. Правильно:exec.Command("ffmpeg", "-i", path, ...). - Экранирование строки двойными кавычками без учёта вложенных кавычек
fmt.Sprintf("echo \"%s\"", msg)— легко обойти. Никогда не экранируйте для shell вручную, это путь к аду. - Доверие к
filepath.Cleanбез проверки инъекции в именах файлов
Спецсимволы shell не обязательно отсекаютсяClean. Передавайте имена файлов как отдельные аргументы.
Возможные дополнительные вопросы интервьюера
Что делать, если приложение должно выполнять сложный конвейер команд (find | sort)?
Выполняйте без shell: можно программно построить конвейер, создав несколько Cmd и связав их StdoutPipe. Это безопаснее, но сложнее. Если shell критичен, используйте sh -c с тщательным экранированием всех параметров через shellquote и санитизацию.
Как обезопасить систему от даже успешной инъекции?
Принцип минимизации прав: запуск процесса под отдельным пользователем с ограниченными capability, использование unshare/pivot_root, системные фильтры (seccomp), блокировка исходящих сетевых соединений, аудит событий.
Может ли filepath.Clean предотвратить command injection?
Нет, filepath.Clean обрабатывает пути (например, убирает ..), но не спасает от символов ;, |, $(). Только разделение аргументов без shell помогает.
Что такое path traversal?
Краткий ответ (30–60 секунд)
Path traversal (или directory traversal) — атака, при которой злоумышленник получает доступ к файлам и директориям за пределами разрешённого каталога, манипулируя относительными путями (../, ..%2F). В Go защита достигается использованием filepath.Clean с проверкой префикса и использование безопасных файловых серверов.
Развёрнутый ответ
Пример уязвимого кода:
filename := r.URL.Query().Get("file")
data, _ := os.ReadFile("/var/app/uploads/" + filename)
При ../../etc/passwd читается системный файл. Исправление:
base := "/var/app/uploads"
cleanPath := filepath.Join(base, filepath.Clean("/"+filename))
if !strings.HasPrefix(cleanPath, filepath.Clean(base)+string(os.PathSeparator)) {
http.Error(w, "Bad path", 400)
return
}
data, _ := os.ReadFile(cleanPath)
Дополнительно: запретить символические ссылки (с проверкой EvalSymlinks), использовать http.FileServer безопасно — http.Dir при открытии проверяет, не выходит ли путь за корень через filepath.Clean и os.Open.
Под капотом (internals)
http.FileServer использует http.Dir, который в методе Open вызывает filepath.Join с корнем и запрашиваемым именем, затем filepath.Clean и проверяет, что результат не содержит .. после очистки (фактически через strings.HasPrefix). filepath.EvalSymlinks может быть применён для разрешения символических ссылок, но может менять путь так, что выходит за корень — нужно повторно проверять префикс. В os.Open ОС не проверяет корень, именно код должен обеспечить барьер. Пакет io/fs в Go 1.16+ предоставляет интерфейс fs.FS, где безопасная реализация Sub (fs.Sub) ограничивает файловую систему поддиректорией, что является надёжным способом предотвращения выхода за корень: sub, _ := fs.Sub(rootFS, "base").
Типичные ошибки кандидатов
- Только
filepath.Cleanбез проверки префикса
Cleanубирает.., но если результат всё равно может оказаться за пределами корня при определённых комбинациях, нужна префиксная проверка. Например,/var/app/uploadsи../../otherдаст/var/app/other, который не имеет требуемого префикса и должен быть отброшен. - Игнорирование URL-декодирования
../может быть закодирован как%2e%2e/, сначала нужно декодировать путь (url.PathUnescape), затем нормализовать. - Использование
ioutil.ReadFileс сырым пользовательским вводом
Устаревшая привычка; даже при проверках лучше применятьfs.Subили обёртки, централизованно ограничивающие доступ.
Возможные дополнительные вопросы интервьюера
Как защититься от path traversal при использовании ZIP-архивов?
При распаковке проверять filepath.Clean для каждого пути в архиве и убеждаться, что он не выходит за целевую директорию. Знаменитая уязвимость Zip Slip — классический пример.
Что такое fs.Sub и как он помогает?
fs.Sub возвращает файловую систему, ограниченную указанным поддеревом. Любые операции через неё автоматически не выйдут за пределы этой поддиректории.
Стоит ли запрещать символические ссылки в загружаемых архивах?
Да, если сервер следует символическим ссылкам при извлечении, злоумышленник может создать ссылку на /etc/passwd. Нужно либо раскрывать ссылки и проверять результирующий путь, либо игнорировать их.
Что такое SSRF?
Краткий ответ (30–60 секунд)
Server-Side Request Forgery — атака, при которой сервер по запросу злоумышленника выполняет сетевой запрос к произвольному внутреннему или внешнему ресурсу. Это позволяет обходить файрволы, получать доступ к облачным метаданным, внутренним API. В Go нужно валидировать URL, запрещать доступ к внутренним IP-адресам, ограничивать перенаправления.
Развёрнутый ответ
Опасный код: сервер загружает картинку по URL, переданному клиентом:
resp, err := http.Get(userURL)
body, _ := io.ReadAll(resp.Body)
Злоумышленник передаёт http://169.254.169.254/latest/meta-data/iam/security-credentials/, получает ключи AWS. Защита в Go:
- Разрешить только внешние HTTP/HTTPS; запретить
file://,gopher://. - Пропустить URL через фильтр: разрезолвить DNS, проверить, что IP не принадлежит приватным диапазонам (
net.IP.IsPrivate), не loopback, не link-local. - Использовать отдельную сетевую обвязку http.Transport с DialContext, который блокирует нежелательные адреса.
- Ограничить количество редиректов и проверять хост каждого редиректа.
- Использовать минимальные привилегии сети: egress firewall, политики Kubernetes.
Реализация проверки:
func isSafeAddress(hostport string) bool {
host, _, _ := net.SplitHostPort(hostport)
ip := net.ParseIP(host)
if ip != nil {
if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() {
return false
}
}
return true
}
Под капотом (internals)
net.Dialer.Control в Go позволяет перехватить низкоуровневое syscall перед connect и проверить IP. net.LookupIP даёт разрешённые адреса. Пакет net кеширует DNS результаты согласно системе; в Linux разрешение имён идёт через glibc или netgo, что добавляет сложности. При использовании кастомного Resolver можно блокировать DNS rebinding: проверять, что после разрешения IP не стал приватным, даже если сначала был допустимым DNS. http.Client при редиректах вызывает CheckRedirect, где нужно повторно проверить новый URL. Пакет x/net/publicsuffix помогает отделить внутренние домены от публичных.
Типичные ошибки кандидатов
- Проверка хоста только в URL до разрешения DNS
DNS rebinding: домен сначала отдаёт публичный IP при проверке, а затем меняет на приватный. Нужно проверять IP-адрес после разрешения при самом соединении. - Запрет только
127.0.0.1, игнорируя0.0.0.0,[::1], IPv6-варианты loopback
net.IP.IsLoopbackпокрывает все, но надо не забыть иIsPrivate,IsLinkLocalUnicast. - Разрешение редиректов без валидации нового хоста
Даже если начальный URL прошёл проверку, 302 может направить на внутренний ресурс.CheckRedirectобязателен.
Возможные дополнительные вопросы интервьюера
Как обрабатывать URL, которые используют DNS rebinding для обхода проверок?
Реализовать DialContext с кастомным Resolver, который после разрешения адреса сверяет IP с белым/чёрным списком, и разрывает соединение, если адрес недопустим. Также можно ограничить время жизни кеша DNS.
Что делать, если бизнес-требование требует доступа к определённым внутренним хостам?
Вести белый список разрешённых внутренних адресов/доменов и проверять принадлежность принимающему IP. Важно ограничить порты, схемы и пути.
Как предотвратить SSRF в окружениях с IPv6?
Нельзя игнорировать IPv6 адреса — обязательно проверять все адреса, возвращаемые net.LookupIP, включая IPv6 варианты loopback и link-local (::1, fe80::). Использовать net.IP.To4() вместе с полной проверкой.
Что такое авторизация (authorization) vs аутентификация (authentication)?
Краткий ответ (30–60 секунд)
Аутентификация отвечает на вопрос «кто ты?» — проверка учётных данных (пароль, токен, сертификат). Авторизация отвечает «что тебе разрешено?» — определение прав доступа к ресурсам после установления личности. Оба процесса должны быть строго разделены в коде и архитектуре.
Развёрнутый ответ
- Аутентификация: процесс подтверждения идентичности. В Go часто через middleware, которое проверяет JWT, сессию, API-ключ и добавляет контекст пользователя (например,
r = r.WithContext(context.WithValue(ctx, userKey, user))). - Авторизация: проверка, имеет ли аутентифицированный пользователь право совершить действие. Реализуется отдельным middleware или внутри handler’а: проверка роли, прав (
user.HasPermission("write")). Модели: RBAC, ABAC, ACL.
Пример разделения:
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := extractToken(r)
user, err := validateToken(token)
if err != nil { http.Error(w, "unauthorized", 401); return }
ctx := context.WithValue(r.Context(), "user", user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func RequireRole(role string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(User)
if !user.HasRole(role) { http.Error(w, "forbidden", 403); return }
next.ServeHTTP(w, r)
})
}
}
Под капотом (internals)
В Go аутентификация часто реализуется на основе стандартного context.Context. Значение сохраняется через context.WithValue. Для масштабирования используют JWT с локальной проверкой подписи (без запроса в БД), но тогда revocation усложняется. Для авторизации с большим количеством ролей рекомендуется использовать отдельный сервис (OPA, Casbin), интегрируемый через middleware. casbin — широко используемая Go-библиотека, поддерживающая модели RBAC/ABAC, с кешированием и распределённым хранением.
Типичные ошибки кандидатов
- Смешение аутентификации и авторизации в одном обработчике
Проверка пары логин/пароль и тут же проверка доступа создают монолит, затрудняют тестирование и переиспользование. Всегда разделяйте middleware. - Возврат 401 для ошибок авторизации
Код 401 «Unauthorized» на самом деле означает отсутствие аутентификации. Для недостаточных прав следует использовать 403 Forbidden. - Передача ролей и прав через параметры запроса или клиентской стороны
Никогда не доверяйте скрытым полям формы или токенам на клиенте для авторизации — все проверки прав на сервере.
Возможные дополнительные вопросы интервьюера
Как реализовать динамическое изменение прав пользователя без логаута?
Можно использовать stateless JWT с небольшим сроком жизни и хранением прав в токене; тогда новые права вступят в силу после обновления токена. Либо при каждом запросе обращаться к центральному сервису авторизации, но это добавляет задержку.
Что такое ABAC и когда использовать вместо RBAC?
Attribute-Based Access Control учитывает атрибуты пользователя, ресурса, окружения (время, местоположение). Полезно для сложных политик: «доступ только в рабочее время из корпоративной сети». В Go может быть реализован через Open Policy Agent (OPA).
Как в микросервисной архитектуре передавать авторизационные данные между сервисами?
Использовать access token с claims о ролях/правах, либо через внутренний JWT (с асимметричной подписью) и проверкой на каждом сервисе. Либо сервис-посредник авторизации, который проверяет разрешение и возвращает решение.
Что такое RBAC?
Краткий ответ (30–60 секунд)
Role-Based Access Control — модель авторизации, в которой права назначаются не пользователям напрямую, а ролям, а пользователи получают роли. Это упрощает управление доступом, особенно в крупных системах. В Go часто реализуется через middleware, проверяющее принадлежность пользователя требуемой роли.
Развёрнутый ответ
Компоненты RBAC:
- User (субъект),
- Role (администратор, редактор, зритель),
- Permission (чтение, запись, удаление),
- Session (активированный набор ролей).
Иерархия ролей: роль может наследовать права другой роли. Пример простейшей проверки в Go:
type User struct { Roles []string }
func (u User) HasRole(role string) bool {
for _, r := range u.Roles { if r == role { return true } }
return false
}
// middleware
func RequireRole(role string) func(http.Handler) http.Handler { ... }
Более продвинутая — с политиками: разрешение «create_post» для роли «editor». Использование библиотеки Casbin:
e, _ := casbin.NewEnforcer("model.conf", "policy.csv")
// model.conf содержит [request_definition], [policy_definition], [role_definition]
// policy.csv: p, editor, post, write
// проверка: ok, _ := e.Enforce("alice", "post", "write")
Под капотом (internals)
Casbin в Go использует модель, определённую в файле, и компилирует её во внутренние структуры. Enforce проходит по всем политикам и ролевым иерархиям; при большом количестве политик применяется RBAC с кешированием через Enforcer.EnableCache. Политики могут храниться в БД через адаптер, Casbin подгружает их и синхронизирует. Внутренне используется govaluate для выражений условий (ABAC). В Go без внешних библиотек RBAC обычно реализуется на middleware уровне с чтением ролей из JWT или сессии. Для быстрых проверок роли кешируются в контексте запроса.
Типичные ошибки кандидатов
- Проверка ролей по строковому названию без учёта иерархии
Если «admin» наследует «user», простая проверкаhasRole("user")может не сработать. Нужно просчитывать полный набор результирующих ролей. - Хранение ролей только на клиенте
JWT может содержать роли, но клиент может их подменить при взломе (если не использовать асимметричную подпись). Всегда проверяйте роли на сервере из подписанного токена или данных сессии. - Отсутствие обновления ролей без перелогина
Если роли закодированы в access токене с долгим сроком жизни, изменение прав не вступит в силу немедленно. Реализуйте механизм инвалидации или короткоживущие токены с refresh.
Возможные дополнительные вопросы интервьюера
Как реализовать динамическое добавление ролей и прав через админку без рестарта сервера?
Храните политики во внешнем хранилище (БД, etcd) и используете библиотеку с горячей перезагрузкой, например Casbin с Watcher или кастомный загрузчик, который обновляет in-memory структуру по сигналу.
RBAC vs ACL: когда что выбрать?
ACL задаёт разрешения уровня пользователь-ресурс, удобен для точного контроля, но плохо масштабируется. RBAC группирует права в роли и упрощает администрирование при росте числа пользователей.
Как тестировать RBAC в Go?
Создавать тестовые JWT с разными ролями, делать запросы через httptest и проверять коды 403/200, а также unit-тесты middleware с mock-объектами.
Что такое симметричное и асимметричное шифрование?
Краткий ответ (30–60 секунд)
Симметричное шифрование использует один и тот же ключ для шифрования и расшифрования (AES, ChaCha20). Быстрое, но требует безопасного обмена ключом. Асимметричное (RSA, ECDH, Ed25519) — пара ключей: открытый и закрытый. Открытый используется для шифрования или проверки подписи, закрытый — для расшифрования или подписи. Медленнее, но решает проблему распределения ключей.
Развёрнутый ответ
-
Симметричное:
-
AES-GCM: шифрует и обеспечивает аутентификацию, используется в TLS. Пример в Go:
Go key := make([]byte, 32) // AES-256 aes, _ := aes.NewCipher(key) gcm, _ := cipher.NewGCM(aes) nonce := make([]byte, gcm.NonceSize()) ciphertext := gcm.Seal(nil, nonce, plaintext, nil) -
Применение: шифрование данных в БД, сессионные cookie, TLS-трафик.
-
-
Асимметричное:
- RSA: шифруют короткие данные (сеансовый ключ) или подписывают. В Go
crypto/rsa. - ECDSA (P-256): подпись, верификация; Ed25519 — более современная и быстрая подпись.
- ECDH (через
crypto/ecdhв Go 1.20+): обмен ключами. -
Пример подписи Ed25519:
Go pub, priv, _ := ed25519.GenerateKey(rand.Reader) sig := ed25519.Sign(priv, message) ok := ed25519.Verify(pub, message, sig)
- RSA: шифруют короткие данные (сеансовый ключ) или подписывают. В Go
-
В реальных системах комбинируют: асимметричный обмен ключом устанавливает симметричный сессионный ключ (гибридное шифрование).
Под капотом (internals)
В Go crypto/aes для AMD64 использует аппаратные инструкции AES-NI через ассемблер, если доступны. GCM-режим реализован в crypto/cipher с помощью gcmFieldElement и умножения в GF(2^128) с использованием PCLMULQDQ. crypto/ecdh теперь нативный, не требует заимствований из crypto/elliptic. Ed25519 работает в constant-time, устойчив к side-channel. crypto/rand читает из /dev/urandom (или getrandom), обеспечивая криптостойкую случайность.
Типичные ошибки кандидатов
- Использование симметричного шифрования без аутентификации (AES-CBC вместо GCM)
CBC без HMAC уязвим к padding oracle атакам. В Gocrypto/cipherне предоставляет GCM для CBC, но нужно обязательно добавлять MAC после шифрования (encrypt-then-MAC). GCM уже включает аутентификацию. - Перепутывание открытого и закрытого ключа при подписи
Для подписи используется закрытый ключ, для верификации — открытый. Нельзя шифровать на открытом ключе для конфиденциальности с использованием Ed25519 — он только для подписей. Для шифрования нужен RSA/ECIES. - Повторное использование nonce в AES-GCM
Катастрофически уязвимо при повторном использовании nonce с тем же ключом: раскрывается аутентификационный ключ и возможна подделка. Nonce должен быть уникальным для каждой операции шифрования.
Возможные дополнительные вопросы интервьюера
Как в Go реализовать гибридное шифрование для безопасной передачи данных?
Генерируем случайный симметричный ключ, шифруем данные AES-GCM, затем шифруем симметричный ключ асимметричным алгоритмом (RSA-OAEP или ECIES). Передаём зашифрованный симметричный ключ и ciphertext вместе. В Go crypto/ecies из x/crypto может использоваться.
Почему Ed25519 предпочтительнее ECDSA P-256?
Ed25519 быстрее, безопаснее в реализации (не подвержен ошибкам генерации nonce), имеет меньшее количество параметров, менее склонен к side-channel атакам. В Go с 1.13 нативная поддержка.
Что такое envelope encryption и как она применяется в облаках?
Многоуровневая модель: мастер-ключ шифрует дата-ключи, которые, в свою очередь, шифруют данные. Упрощает ротацию ключей и управление доступом. В Go менеджеры ключей (KMS) интегрируются через gocloud.dev/secrets или SDK облачных провайдеров.
Как работать с секретами в Go (переменные окружения, vault)?
Краткий ответ (30–60 секунд)
Секреты (пароли, API-ключи) никогда не хардкодить в коде. Переменные окружения — простейший способ для конфигурации, но имеют ограничения. Для продакшена используют системы управления секретами (HashiCorp Vault, облачные KMS). В Go применяют пакеты os.Getenv, envconfig, либо клиенты к Vault/KMS, получающие секреты динамически и кеширующие их в памяти.
Развёрнутый ответ
- Переменные окружения:
os.Getenv("DB_PASSWORD"). Плюс: простота, поддержка 12-factor app. Минусы: легко утекают (дампы окружения, дочерние процессы наследуют), нет версионирования и ротации. - Файлы конфигурации (
.env): загружаются черезgodotenv. Должны быть исключены из VCS (.gitignore). -
Менеджеры секретов:
-
Vault:
vault/api— клиент на Go, аутентификация через AppRole, токен или Kubernetes auth.Go client, _ := api.NewClient(api.DefaultConfig()) client.SetToken("s.abc...") secret, _ := client.Logical().Read("secret/data/db") password := secret.Data["data"].(map[string]interface{})["password"].(string) -
AWS Secrets Manager / GCP Secret Manager: SDK клиенты.
-
-
Безопасное использование: минимизировать время жизни секрета в памяти; по возможности использовать
memguardдля защиты sensitive данных от свапирования на диск. - В Go секреты не должны логироваться: избегать
fmt.Printf("%s", password), использоватьstring(подавление)илиredact-функции.
Под капотом (internals)
Переменные окружения в Linux хранятся в адресном пространстве процесса, наследуются через fork+exec. Go запущенный процесс может читать окружение через /proc/self/environ. Утечка возможна через /proc/pid/environ (доступ ограничен, но риски есть). Клиент Vault в Go использует HTTP/HTTPS с автоматическим обновлением токена, renewer в горутине. Внутри api.Secret содержит LeaseDuration и Renewable; можно запустить LifetimeWatcher. Многие используют библиотеку envconfig для парсинга структур из окружения с тегами.
Типичные ошибки кандидатов
- Хардкод секретов в коде и коммит в репозиторий
Даже удаление позже — остаётся в истории git. Использовать до коммитаgit-secretsили pre-commit хуки, но главное — сразу выносить. - Передача секретов через аргументы командной строки
Аргументы видны вps aux,/proc/pid/cmdlineвсем пользователям системы. Только окружение или файл. - Логирование полных тел запросов/ответов с токенами
По умолчанию HTTP клиент может залогировать заголовокAuthorization. Нужно реализовать фильтрацию логов или использоватьhttputilс маскированием.
Возможные дополнительные вопросы интервьюера
Как автоматически ротировать секреты в Go-приложении без остановки?
Использовать Vault с динамическими секретами (БД) и Renewer; можно также подписываться на уведомления о новой версии секрета и обновлять через атомарный указатель (atomic.Value), перезагружая соединения.
Что такое memguard и зачем он нужен?
Библиотека для хранения sensitive данных в защищённой области памяти, которая блокирует свапирование на диск и затирает после освобождения. Защищает от дампов памяти и cold boot атак.
Как с Go безопасно передать секрет в контейнеризированной среде?
Использовать Kubernetes Secrets, монтируемые как файлы или переменные окружения; в init-контейнере получать из Vault с помощью auth role и записывать в shared emptyDir volume, доступный только контейнеру приложения.
Что такое "man-in-the-middle" атака?
Краткий ответ (30–60 секунд)
MITM — атака, при которой злоумышленник тайно перехватывает и, возможно, изменяет коммуникацию между двумя сторонами, которые считают, что общаются напрямую. Защита обеспечивается TLS с надёжной проверкой сертификатов и взаимной аутентификацией.
Развёрнутый ответ
Классическая реализация: злоумышленник находится в одной сети (ARP spoofing), перехватывает трафик. При HTTP видит всё в открытом виде. При HTTPS, если клиент не проверяет сертификат (или установлен подставной CA), атакующий подменяет сертификат и расшифровывает трафик (SSL stripping, sslsplit). В Go защита:
tr := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: systemCerts, // никогда InsecureSkipVerify: true
},
}
client := &http.Client{Transport: tr}
Также важно фиксировать публичные ключи (pinning) для критичных соединений.
Под капотом (internals)
В Go, если RootCAs пул пустой, используется системный пул. InsecureSkipVerify полностью отключает проверку — handshake пройдёт с любым сертификатом. Пакет crypto/tls выполняет рукопожатие: клиент проверяет сертификат сервера вызовом cert.Verify. MITM невозможен, пока корневой CA не скомпрометирован. Атака через подстановку CA требует установки корневого сертификата на устройство жертвы (корпоративные прокси). В Go можно добавить проверку на известный публичный ключ через VerifyPeerCertificate, что предотвращает даже легитимно выданные сертификаты для фишинговых доменов.
Типичные ошибки кандидатов
InsecureSkipVerify: trueдля обхода ошибок сертификата в продакшене
Это приглашение для MITM. Все проблемы с сертификатами должны решаться правильной настройкой цепочек, а не отключением проверок.- Не проверка ошибки TLS при использовании библиотек
Например,http.DefaultClientиспользует дефолтные настройки и вернёт ошибку при плохом сертификате, но многие игнорируют ошибкуerr, полагая, что соединение установлено. - Доверие к системному пулу без проверки отзыва сертификатов (OCSP/CRL)
Украденный сертификат будет принят, пока не истечёт. В критических системах нужно активно проверять статус отзыва.
Возможные дополнительные вопросы интервьюера
Что делает SSL Pinning и как его реализовать в Go?
Фиксируется ожидаемый публичный ключ или хеш сертификата, и при handshake проверяется соответствие. Реализуется через tls.Config.VerifyPeerCertificate. Минус — сложность обновления.
Как ARP spoofing может облегчить MITM в локальной сети?
Атакующий подменяет MAC-адрес шлюза, заставляя жертву отправлять трафик через свою машину. Защита — сетевые уровни (802.1X), статические ARP записи, шифрование каналов.
Почему даже с HTTPS возможен MITM через корпоративный прокси?
Корпоративный прокси подменяет сертификаты, имея свой собственный CA, установленный в хранилище устройства. Для Go-клиента это выглядит как законный сертификат. Предотвратить можно через SSL pinning.
Что такое DDoS и как защититься?
Краткий ответ (30–60 секунд)
Distributed Denial of Service — атака, направленная на перегрузку ресурсов сервера (сеть, CPU, память) с множества машин, делая сервис недоступным. Защита многоуровневая: фильтрация на границе сети, rate limiting, CDN/WAF, масштабирование, мониторинг аномалий, оптимизация кода.
Развёрнутый ответ
Типы DDoS: объёмные (флуд трафика), протокольные (SYN flood, UDP amplification), прикладные (HTTP flood, медленные атаки Slowloris). В Go защитные меры:
- Rate limiting: пакет
golang.org/x/time/rateдля лимитирования запросов по IP/токену. - Таймауты на
http.Server:ReadTimeout,WriteTimeout,IdleTimeout, предотвращают удержание соединений. - Ограничение размера полезной нагрузки:
http.MaxBytesReader. - Инфраструктурные: развёртывание за reverse прокси (nginx, Envoy), CDN (Cloudflare, AWS Shield), балансировщики, BGP blackholing.
- Мониторинг: Prometheus метрики, алерты при росте 500-х ошибок и потребления ресурсов.
Пример настройки сервера:
srv := &http.Server{
Addr: ":8080",
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
Под капотом (internals)
x/time/rate реализует token bucket алгоритм с возможностью burst. Лимитер хранит состояние в памяти, потокобезопасен. net/http сервер использует горутину на каждое соединение; idle-соединения удерживаются, пока не истечёт IdleTimeout. MaxBytesReader ограничивает чтение тела запроса, оборачивая io.LimitReader. При превышении лимита возвращается ошибка, не давая врагу исчерпать память.
Типичные ошибки кандидатов
- Отсутствие ограничения размера тела запроса
Злоумышленник отправляет многогигабайтный POST, раздувая память. Всегда применятьhttp.MaxBytesReader. - Слишком большие таймауты или их отсутствие
По умолчаниюhttp.Serverимеет бесконечные таймауты, что делает уязвимым для Slowloris. Явная установка обязательна. - Rate limiting только на уровне приложения без сетевой защиты
Объёмный флуд забьёт канал до того, как дойдёт до приложения. Необходима фильтрация на более раннем уровне.
Возможные дополнительные вопросы интервьюера
Чем отличается токен бакет от leaky bucket, и когда что применять в Go?
Token bucket позволяет кратковременные всплески (burst), leaky bucket сглаживает трафик с фиксированной скоростью. Для API часто используют token bucket (x/time/rate), позволяя небольшой burst, но ограничивая среднюю частоту.
Как реализовать распределённый rate limiting для нескольких экземпляров приложения?
Использовать централизованное хранилище (Redis) с алгоритмом скользящего окна или GCRA. Go клиент Redis может применять Eval для выполнения Lua-скриптов, атомарно проверяющих счётчики.
Какие метрики важнее всего для обнаружения DDoS на уровне приложения?
Количество запросов по IP/ендпоинтам, 5xx/429 коды, задержка обработки, утилизация памяти/горутин. Резкий рост 429 (rate limit) — индикатор атаки.
Как работает HTTPS и почему это важно?
Краткий ответ (30–60 секунд)
HTTPS — это HTTP поверх TLS. Обеспечивает шифрование, целостность и аутентификацию сервера (а опционально и клиента). Предотвращает перехват, подмену данных и выдачу себя за сервер. Важен, потому что без него любой посредник может видеть и изменять трафик, что катастрофично для конфиденциальности и безопасности.
Развёрнутый ответ
Работа:
- Клиент инициирует TCP-соединение на порт 443.
- TLS handshake (см. вопрос 6):
- Согласование версии, cipher suite.
- Сервер предъявляет сертификат, клиент проверяет его.
- Обмен по DH, генерируется сессионный ключ.
- Далее HTTP-сообщения шифруются симметричным алгоритмом.
- При закрытии соединение завершается.
Важность:
- Конфиденциальность: никто не видит пароли, номера карт, личные сообщения.
- Целостность: данные не могут быть изменены (Mallory-in-the-middle).
- Аутентичность: пользователь уверен, что общается именно с
example.com, а не с подделкой. - SEO и доверие: браузеры помечают HTTP как «небезопасный».
В Go переход на HTTPS прост:
http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil)
Под капотом (internals)
ListenAndServeTLS автоматически настраивает tls.Config с сертификатами и вызывает tls.NewListener. По умолчанию используется http.DefaultServeMux. Внутри tls.Conn реализует net.Conn, выполняя шифрование прозрачно. Ключевые особенности: Session Tickets для возобновления сессий (включено по умолчанию). Go поддерживает 0-RTT данные в TLS 1.3 (ранние данные), но это нужно включать явно. Всё работает на pure Go (без OpenSSL), что упрощает кросс-компиляцию.
Типичные ошибки кандидатов
- Использование HTTP в продакшене
Даже для внутренних сервисов должен использоваться HTTPS, так как внутри сети может быть защита периметра, но не нулевое доверие. Все коммуникации должны быть зашифрованы. - Просроченные или самоподписанные сертификаты на клиентских серверах
Приводит к ошибкам соединения или к необходимостиInsecureSkipVerify, что нивелирует безопасность. Автоматизируйте выпуск через ACME. - Забытые редиректы с HTTP на HTTPS
Клиенты могут случайно зайти по HTTP, и трафик уязвим до редиректа. Нужно настраивать HSTS, а также закрывать 80 порт.
Возможные дополнительные вопросы интервьюера
Как работает HTTP/2 и HTTP/3 поверх TLS в Go?
HTTP/2 в net/http включается автоматически при использовании TLS через tls.Config.NextProtos и ALPN. HTTP/3 (QUIC) пока не в стандартной библиотеке, но есть quic-go — требует TLS 1.3, использует UDP.
Почему не стоит применять HTTPS только на «чувствительных» страницах?
Смесь HTTP и HTTPS приводит к риску sidejacking (кража сессионных кук, передаваемых по HTTP). Лучше полный HTTPS.
Как реализовать автоматический редирект HTTP→HTTPS в Go?
Завести отдельный HTTP-сервер на 80, который делает 301 редирект на https-версию и добавляет HSTS заголовок.
Что такое "сессии" и как их безопасно реализовать?
Краткий ответ (30–60 секунд)
Сессия — это состояние пользователя, сохраняемое между HTTP-запросами. Обычно реализуется через сессионный идентификатор, хранящийся в защищённой cookie, и серверное хранилище (память, Redis). Безопасность обеспечивается флагами HttpOnly, Secure, SameSite, криптостойкой случайностью ID, фиксацией IP/User-Agent и ограничением времени жизни.
Развёрнутый ответ
Основные принципы безопасной сессии:
- Случайный идентификатор: генерировать с помощью
crypto/rand(неmath/rand), длина не менее 128 бит, кодировать в base64. - Cookie атрибуты:
HttpOnly: true— невидима для JavaScript (защита от XSS).Secure: true— передаётся только по HTTPS.SameSite: LaxилиStrict— защита от CSRF.Path=/— ограничить область.
- Хранение сессионных данных:
- Для простых приложений — in-memory (с осторожностью, при перезапуске теряются).
- Для продакшена — Redis, memcached, БД.
- Не хранить sensitive данные в самом cookie (кроме ID).
- Безопасные таймеры: абсолютный и idle таймауты, при logout инвалидация на сервере.
- Привязка к окружению: можно хранить IP, User-Agent; при изменении требовать перелогин (но осторожно с мобильными сетями).
Пример инициализации сессии на Go (упрощённо):
sessionID := base64.URLEncoding.EncodeToString(randomBytes(32))
http.SetCookie(w, &http.Cookie{
Name: "sid",
Value: sessionID,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
// сохранить sessionID -> data в хранилище
Под капотом (internals)
Пакет net/http напрямую не предоставляет сессий. Библиотека gorilla/sessions использует подписанные куки (через HMAC) для хранения данных в cookie, что позволяет избежать серверного хранилища, но размер ограничен 4KB. Подпись через securecookie гарантирует целостность и конфиденциальность (опционально шифрует). Для серверных сессий scs (алгоритм session management) генерирует ID, хранит в Redis, автоматически управляет временем жизни. crypto/rand читает /dev/urandom — не блокируется и безопасен.
Типичные ошибки кандидатов
- Предсказуемая генерация ID сессии (например,
math/rand)
Злоумышленник может подобрать следующий ID и захватить сессию. Толькоcrypto/rand. - Отсутствие
HttpOnlyиSecureфлагов
Делает возможным кражу через XSS и передачу по HTTP, что сводит на нет всю безопасность. - Бесконечный срок жизни сессии
Увеличивает окно атаки. Всегда задавайте абсолютный таймаут (например, 24 часа) и idle таймаут (30 минут), продлевая только при активности. - Хранение данных сессии в виде plain JSON в cookie без шифрования/подписи
Пользователь может подменить данные, если нет криптографической проверки. Использовать подписанные куки или серверное хранилище.
Возможные дополнительные вопросы интервьюера
Как бороться с кражей сессии через 2FA?
Даже при угоне сессии, для критических действий запрашивать второй фактор (текущий). В Go — middleware запрашивает OTP, затем временный повышенный уровень привилегий.
Что такое сессионная фиксация и как её предотвратить?
Атакующий даёт жертве известный ID сессии до логина, потом после аутентификации жертвы сам заходит под этим ID. Защита: ротировать ID сессии после логина (session.RegenerateID в scs или кастомный код).
Безопасно ли хранить роли пользователя прямо в сессионном cookie?
Только если cookie подписана/зашифрована и серверная проверка всё равно присутствует. Лучше держать роли на сервере и сверять.
Что такое SAML?
Краткий ответ (30–60 секунд)
Security Assertion Markup Language — XML-стандарт для обмена аутентификационными и авторизационными данными между Identity Provider (IdP) и Service Provider (SP). Позволяет реализовать Single Sign-On (SSO) в корпоративных средах. В Go используется через crewjam/saml или mattermost/xml-roundtrip-validator.
Развёрнутый ответ
Акторы:
- IdP (Okta, ADFS, Keycloak) — аутентифицирует пользователя.
- SP (ваше приложение) — доверяет утверждениям от IdP.
Процесс:
- Пользователь заходит на SP.
- SP перенаправляет (HTTP-Redirect) на IdP с AuthnRequest.
- Пользователь логинится на IdP.
- IdP отправляет ответ (POST) с SAML Assertion на ACS URL в SP.
- SP проверяет XML подпись, извлекает атрибуты (имя, email, группы), создаёт сессию.
В Go библиотека crewjam/saml реализует SP и частично IdP. Настройка:
sp, _ := samlsp.New(samlsp.Options{
URL: *rootURL,
Key: *key,
Certificate: *cert,
IDPMetadata: idpMetadata,
})
app.Handle("/saml/", sp)
Под капотом (internals)
crewjam/saml использует encoding/xml с аннотациями для SAML-схем. При POST-обработке проверяется подпись всего ответа и assertion с использованием crypto/x509; проверяются условия NotBefore/NotOnOrAfter, получатель (Audience), статус ответа. Хранилище сессий может быть представлено cookie (подписанный JWT) или серверное. IdP-метаданные парсятся из XML и кешируются.
Типичные ошибки кандидатов
- Принятие ассертов без проверки подписи
Злоумышленник может подделать XML. Всегда проверяйте цифровую подпись с доверенным сертификатом IdP. - Неправильная проверка
RecipientиAudience
Позволяет использовать assertion, предназначенный для другого SP. Обязательно сверятьRecipientс ACS URL,Audienceс entity ID. - Доверие к непроверенным атрибутам
После верификации SAML нужно извлекать атрибуты, но затем применять собственную авторизацию на основе ролей, а не полагаться слепо на переданные группы.
Возможные дополнительные вопросы интервьюера
Чем SAML отличается от OIDC?
SAML тяжеловесен (XML, SOAP), чаще используется в enterprise. OIDC проще, базируется на OAuth2, JSON, лучше подходит для мобильных и SPA приложений.
Как реализовать SP-initiated и IdP-initiated SSO?
В Go библиотека поддерживает оба. IdP-initiated начинается с IdP, который посылает unsolicited response; SP должен уметь его принять без запроса.
Как обработать выход из системы (Single Logout)?
SAML SLO требует обмена LogoutRequest/Response между SP и IdP. Библиотека crewjam/saml обеспечивает генерацию и обработку этих сообщений, инвалидируя локальную сессию.
Что такое "Content Security Policy" (CSP)?
Краткий ответ (30–60 секунд)
CSP — HTTP-заголовок (Content-Security-Policy), позволяющий серверу объявить, из каких источников можно загружать ресурсы (скрипты, стили, изображения) и какие действия разрешены (inline eval, плагины). Это мощная защита от XSS и data injection. В Go заголовок устанавливается через w.Header().Set.
Развёрнутый ответ
Директивы:
default-src 'self'— по умолчанию только с того же origin.script-src 'self' https://trusted.cdn.com 'nonce-abc123'— разрешённые источники скриптов + nonce для inline.style-src 'self' 'unsafe-inline'— стили (осторожно с inline).img-src *— изображения отовсюду.frame-ancestors 'none'— для защиты от clickjacking (X-Frame-Options аналог).upgrade-insecure-requests— автоматический апгрейд HTTP до HTTPS.report-uri /csp-report— отправка отчётов о нарушениях.
Пример в Go:
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'nonce-xyz'")
next.ServeHTTP(w, r)
})
}
Nonce генерируется на каждый запрос и вставляется в шаблон:
nonce := base64.StdEncoding.EncodeToString(randomBytes(16))
w.Header().Set("Content-Security-Policy", fmt.Sprintf("script-src 'self' 'nonce-%s'", nonce))
// в шаблоне: <script nonce="{{.Nonce}}">...</script>
Под капотом (internals)
Браузер анализирует заголовок CSP и пропускает только то, что соответствует политике. Nonce-подход требует уникальный nonce для каждого ответа; html/template может автоматически вставлять nonce при помощи кастомных функций. Go сам не генерирует nonce, это делает разработчик.
Типичные ошибки кандидатов
- Использование
unsafe-inlineбез необходимости
Разрешает любые inline-скрипты/стили, что делает CSP бесполезным против XSS. Лучше nonce или hash. - Установка заголовка только на главную страницу, не для всех ответов
Если какая-то страница уязвима, CSP должен быть на всех. Middleware должен применяться глобально. report-uriуказывает на тот же домен без обработки
Можно переполнить логи отчётами о нарушениях и пропустить реальные атаки. Нужно мониторить и фильтровать.
Возможные дополнительные вопросы интервьюера
Как CSP предотвращает эксфильтрацию данных?
Даже если XSS существует, CSP блокирует загрузку внешних скриптов и подключение к сторонним ресурсам (connect-src), предотвращая отправку украденных данных на сервер злоумышленника.
Что такое Trusted Types и как связаны с CSP?
Trusted Types требуют, чтобы опасные функции (innerHTML, document.write) принимали только объекты специального типа, а не строки. Усиливает CSP против DOM XSS. В Go надо лишь добавить require-trusted-types-for 'script' в заголовок.
Можно ли использовать CSP в API-только приложении?
Да, для защиты от MIME-сниффинга и clickjacking через frame-ancestors и default-src none. Даже для JSON-ответов полезно.
Как работает x/crypto/argon2?
Краткий ответ (30–60 секунд)
Argon2 — современная, победившая в Password Hashing Competition функция формирования ключа, устойчивая к GPU/ASIC атакам за счёт интенсивного использования памяти. Пакет golang.org/x/crypto/argon2 предоставляет IDKey, Key для генерации хеша заданной длины. Параметры: time, memory, threads.
Развёрнутый ответ
Argon2 имеет варианты: i (устойчив к side-channel), d (устойчив к GPU-cracking), id (гибрид). Рекомендуется argon2id. Функция argon2.IDKey(password, salt, time, memory, threads, keyLen):
time— количество итераций (проходов),memory— размер используемой памяти в килобайтах,threads— степень параллелизма.
Пример хеширования пароля:
password := []byte("userpass")
salt := make([]byte, 16)
rand.Read(salt)
hash := argon2.IDKey(password, salt, 1, 64*1024, 4, 32)
// Сохранить в формате: $argon2id$v=19$m=65536,t=1,p=4$<salt_b64>$<hash_b64>
Верификация: извлечь параметры и соль из сохранённой строки, повторить вызов IDKey и сравнить через subtle.ConstantTimeCompare.
Под капотом (internals)
Реализация на чистом Go в x/crypto/argon2 основана на алгоритме Blake2b для инициализации. Используется схема, заполняющая массив памяти псевдослучайными блоками с зависимостью от пароля, соли и предыдущих блоков. Управляемая память делает невозможным распараллеливание на многоядерных GPU с ограниченной видеопамятью. Функция processBlock оперирует 1024-байтовыми блоками, вызывая blake2b и XOR операции. Внутренние циклы оптимизированы, но не используют ASM-вставки; производительность приемлема для серверных задач.
Типичные ошибки кандидатов
- Использование слишком маленькой памяти (
memory=16KiB)
Делает Argon2 уязвимым к атакам с ASIC. Минимальная рекомендация OWASP на 2023 год: 46 МБ (47104KiB). Желательно ещё больше. - Ручное кодирование выходного формата без учёта обратной совместимости
Стандартный формат PHC помогает в будущем менять параметры. Сохраняйте в кодированном виде с идентификатором версии. - Сравнение хешей через
bytes.Equal
Необходимо использоватьsubtle.ConstantTimeCompareдля предотвращения timing-атак.
Возможные дополнительные вопросы интервьюера
Как выбрать параметры Argon2 для продакшена?
Использовать рекомендованные OWASP: time=1 (или 2 при возможности), memory=47104, threads=1 (на сервере с большим количеством горутин лучше 1, чтобы избежать чрезмерной конкуренции). Главное — чтобы общее время занимало ~0.5 с на целевом железе.
В чём разница между Argon2id и bcrypt?
Argon2id memory-hard, что даёт преимущество перед bcrypt при атаках с FPGA/ASIC. Bcrypt ограничен 4 KiB памяти и не защищает от специализированного железа.
Можно ли использовать Argon2 для вывода ключей шифрования?
Да, argon2.Key генерирует ключ из пароля и соли для использования в шифровании (AES). Применяется, например, в утилитах шифрования файлов.
Что такое "радужные таблицы" и как соль их предотвращает?
Краткий ответ (30–60 секунд)
Радужная таблица — это предварительно вычисленная база вида «открытый текст → хеш» для быстрого обратного поиска паролей по их хешам. Соль — уникальная случайная строка, добавляемая к паролю перед хешированием, что делает каждый хеш уникальным даже для одинаковых паролей и разрушает эффективность радужных таблиц.
Развёрнутый ответ
Атака без соли: злоумышленник имеет дамп хешей. Он ищет хеши в готовой таблице, мгновенно находя общие пароли. Соль (os.Getenv("HEX_SALT")) превращает H(password) в H(salt + password). Даже у двух пользователей с паролем "12345" хеши будут разными из-за разных солей. Радужные таблицы нужно строить для каждой уникальной соли, что вычислительно нереально, если соль имеет достаточную длину (16+ байт). Современные адаптивные хеш-функции (bcrypt, Argon2) включают генерацию соли автоматически.
В Go с bcrypt:
hash, _ := bcrypt.GenerateFromPassword([]byte(pwd), cost)
// соль включена в хеш
Под капотом (internals)
Соль должна быть криптографически стойкой случайной, генерируемой через crypto/rand. Даже использование предсказуемого счетчика (user ID) помогает против глобальных радужных таблиц, но не против целенаправленной атаки на конкретного пользователя. Bcrypt хранит соль в формате Modular Crypt Format вместе со стоимостью и хешем. Argon2 также включает соль в выводе. Понятие pepper (секретный серверный ключ, добавляемый к паролю дополнительно) ещё больше усложняет атаку, но требует безопасного управления.
Типичные ошибки кандидатов
- Постоянная соль для всех пользователей
Если соль одинаковая, злоумышленник построит одну радужную таблицу для этой соли и взломает всех сразу. Соль должна быть разной для каждого экземпляра. - Слишком короткая соль (4 символа)
Современные атаки с GPU могут вычислить таблицу для коротких солей. Минимум 16 байт (128 бит) случайных данных. - Хранение соли отдельно и надежда на её скрытность
Соль не секретна; её назначение — уникальность. Часто хранится в БД рядом с хешем. Секретность соли не должна рассматриваться как защита.
Возможные дополнительные вопросы интервьюера
Что такое перец (pepper) и когда его применять?
Pepper — секретный ключ, одинаковый для всех, хранится вне БД (например, в Vault). Добавляется к паролю вместе с солью. Усложняет брутфорс даже при утечке БД, но требует управления ключом и возможности его ротации без потери доступа.
Радужные таблицы эффективны против bcrypt?
Нет, bcrypt включает соль и настраиваемую стоимость, что делает построение таблиц вычислительно невозможным из-за огромного количества вариантов и длительного времени вычисления одного хеша.
Почему math/rand не подходит для генерации соли?
Его состояние можно предсказать, зная seed (например, время старта). Атакующий может восстановить последовательность и сузить перебор. Только crypto/rand.
Как работает x/crypto/nacl/secretbox?
Краткий ответ (30–60 секунд)
secretbox — аутентичное шифрование (AEAD) на основе Curve25519, Salsa20 и Poly1305, реализованное в пакете golang.org/x/crypto/nacl/secretbox. Шифрует данные с аутентификацией, используя общий 32-байтовый ключ и 24-байтовую nonce. Гарантирует конфиденциальность и целостность, устойчив к tampering.
Развёрнутый ответ
Использование в Go:
var key [32]byte
rand.Read(key[:])
var nonce [24]byte
rand.Read(nonce[:])
message := []byte("данные")
encrypted := secretbox.Seal(nil, message, &nonce, &key)
// расшифрование:
decrypted, ok := secretbox.Open(nil, encrypted, &nonce, &key)
if !ok { /* неверный шифртекст */ }
Sealпринимает дополнительные данные (префикс), но обычно передаютnil. Возвращает зашифрованные данные с добавленным MAC в конце.Openпроверяет аутентичность и расшифровывает, возвращаетfalseпри несовпадении.
Под капотом (internals)
Основан на NaCl (Networking and Cryptography library) от Daniel Bernstein. secretbox реализует алгоритм XSalsa20 для шифрования и Poly1305 для MAC. XSalsa20 — расширение Salsa20 с 192-битной nonce, устойчив к коллизиям. В Go secretbox.Seal вычисляет под-ключ из основного ключа и nonce через HSalsa20, затем шифрует данные потоковым шифром Salsa20 и считает Poly1305-MAC от зашифрованных данных, используя однократный ключ аутентификации. Open проверяет MAC в constant-time и расшифровывает. Всё написано на Go, оптимизированы ассемблерные версии для AMD64 (crypto/salsa20/salsa).
Типичные ошибки кандидатов
- Повторное использование nonce с тем же ключом
При повторении nonce злоумышленник может вычислить разницу между сообщениями и потенциально раскрыть ключ аутентификации. Nonce должен быть уникальным для каждого вызова (обычно генерируется случайно, можно использовать счётчик). - Не проверка возвращаемого
okвsecretbox.Open
Если не проверить, можно использовать поддельные данные, т.к. без проверки расшифрованный мусор неотличим от валидных данных. - Использование секретного бокса без понимания, что он не защищает от подмены nonce
Злоумышленник может повторно отправить старый корректный пакет, если nonce не проверяется на уникальность на уровне протокола. Нужен механизм защиты от replay (временные метки, счётчики).
Возможные дополнительные вопросы интервьюера
Чем отличается secretbox от box?
secretbox — симметричное шифрование (общий ключ). box — асимметричное (публичный/закрытый ключ), использует Curve25519 для обмена и также Salsa20+Poly1305.
Как реализовать защиту от replay поверх secretbox?
В сообщение включить монотонно возрастающий счётчик или временную метку, передавать его в открытом виде, но защищать аутентификацией. Получатель проверяет возрастание и отбрасывает повторы.
Можно ли использовать secretbox для шифрования больших потоков?
Он ориентирован на сообщения, всё сообщение целиком в памяти. Для потоков используют crypto/tls или строят сегментированное шифрование с порядковыми номерами.
Что такое "удостоверяющий центр" (CA) и цепочка доверия?
Краткий ответ (30–60 секунд)
Certificate Authority — доверенная организация, выпускающая цифровые сертификаты. Цепочка доверия строится от конечного сертификата через промежуточные CA к самоподписанному корневому сертификату, который находится в хранилище доверия ОС или браузера. Каждое звено подписано вышестоящим.
Развёрнутый ответ
Структура:
- Корневой CA: самоподписанный, хранится офлайн; его публичный ключ встроен в системы.
- Промежуточный CA: подписан корневым или вышестоящим; используется для выпуска конечных сертификатов.
- Конечный сертификат: подписан промежуточным CA.
Проверка:
- Сервер отправляет конечный сертификат и часто цепочку промежуточных.
- Клиент (браузер, Go-клиент) строит путь до корневого сертификата.
- Проверяет подписи, сроки, Extended Key Usage.
- Проверяет, что корневой CA есть в доверенном пуле.
В Go настройка:
roots := x509.NewCertPool()
roots.AddCert(rootCA)
opts := x509.VerifyOptions{Roots: roots}
_, err := cert.Verify(opts)
Под капотом (internals)
Go при верификации цепочки использует x509.Certificate.Verify, который выполняет:
- Поиск всех возможных цепочек алгоритмом DFS, начиная с конечного сертификата, вплоть до корневого.
- Для каждого сертификата в цепочке проверяется
SignatureAlgorithm, егоBasicConstraints.IsCA,KeyUsage,NameConstraintsи время действия. DNSNameконечного проверяется по SAN/CN.- Результат — список цепочек; обычно берётся первая.
Go не проверяет CRL/OCSP автоматически (нужно явно).
Типичные ошибки кандидатов
- Установка своего CA как корневого без учёта безопасности
Компрометация ключа CA приводит к возможности выпуска сертификатов для любых сайтов. Хранить ключи CA в HSM, строго ограничивать доступ. - Создание цепочки, где промежуточный сертификат имеет
BasicConstraints CA: FALSE
Некорректно, браузер/клиент отклонит цепочку. Нужно при генерации задаватьIsCA: trueдля промежуточных. - Установка длинных сроков действия сертификатов (10 лет)
В случае компрометации ключа долгий срок усугубляет последствия. Рекомендуются короткие сроки и ротация.
Возможные дополнительные вопросы интервьюера
Что такое промежуточный сертификат и почему его нужно отправлять клиенту?
Промежуточный сертификат подписывает конечный, но сам не находится в корневом хранилище клиента. Сервер должен предоставлять цепочку до корня, чтобы клиент мог провести полную проверку.
Как работает Let’s Encrypt и его цепочка доверия?
Let’s Encrypt имеет корень ISRG Root X1, который был добавлен в основные хранилища. Промежуточные сертификаты Let’s Encrypt Authority X3 (устарел) и R3/R4 подписывают конечные. Клиент автоматически строит цепочку.
Как в Go реализовать собственный CA для внутренних сервисов?
Сгенерировать корневой самоподписанный сертификат (x509.CreateCertificate), распространить его публичный ключ на все сервисы. Выпускать клиентские и серверные сертификаты, подписанные этим CA. Использовать mTLS.
Что такое "perfect forward secrecy" (PFS)?
Краткий ответ (30–60 секунд)
Свойство протокола, при котором компрометация долговременного закрытого ключа сервера не раскрывает прошлые сессионные ключи. Достигается использованием эфемерных ключевых обменов (ECDHE, DHE), где сессионный ключ вырабатывается из эфемерных ключей, удаляемых после сессии.
Развёрнутый ответ
Без PFS (например, RSA key exchange): сессионный ключ шифруется публичным ключом сервера. Если позже закрытый ключ сервера украдут, злоумышленник расшифрует все сохранённые трафики. С PFS (ECDHE) каждая сессия генерирует случайную пару эфемерных ключей, ключ сессии вычисляется через обмен ECDHE. Даже если долговременный ключ для подписи украден, сессионные ключи не раскрываются, потому что эфемерные закрытые ключи не сохраняются.
TLS 1.3 поддерживает только PFS: все cipher suites используют ECDHE или DHE. В Go это автоматически:
// TLS 1.3 по умолчанию использует PFS
ln, _ := tls.Listen("tcp", ":443", &tls.Config{
Certificates: []tls.Certificate{cert},
})
Под капотом (internals)
В crypto/tls при handshake 1.3 клиент и сервер генерируют эфемерные ключи (X25519 или P-256) для ecdhe. Они используют crypto/ecdh для генерации и обмена. После выработки общего секрета он пропускается через HKDF для получения сессионных ключей. Эфемерные закрытые ключи существуют только в памяти на время handshake и потом могут быть обнулены (через runtime.KeepAlive и сборку мусора). RSA-обмен в Go для TLS 1.2 по умолчанию исключён, поддерживаются только ECDHE-наборы.
Типичные ошибки кандидатов
- Настройка сервера с поддержкой TLS 1.0–1.2 без ECDHE cipher suites
Если вtls.Config.CipherSuitesвручную указать RSA-наборы (не делайте так без крайней нужды), теряется PFS. Go по умолчанию безопасен. - Сохранение сессионных ключей в логах
Даже при PFS, если приложение логирует симметричные ключи после установки, они могут быть украдены. - Использование статического ECDH (не эфемерного) в собственных протоколах
При самописной криптографии на основе ECDH важно каждый раз генерировать новую пару.
Возможные дополнительные вопросы интервьюера
Почему PFS важна для соответствия PCI DSS?
PCI DSS требует защиты past sessions. Если злоумышленник получил доступ к ключам сервера, PFS гарантирует, что предыдущие транзакции не будут расшифрованы.
Как отключить устаревшие обмены для максимальной PFS в Go?
Использовать MinVersion: tls.VersionTLS13, либо для 1.2 оставить только ECDHE-наборы через CipherSuites. Явно запретить TLS 1.0/1.1.
Покрывает ли PFS атаку «harvest now, decrypt later» квантовыми компьютерами?
Нет, квантовые компьютеры могут в будущем расшифровать сохранённые сессии, если используются классические алгоритмы (ECDHE). Поэтому развиваются постквантовые алгоритмы, Go постепенно интегрирует их.
Что такое security.txt и для чего он нужен?
Краткий ответ (30–60 секунд)
security.txt — стандартизированный текстовый файл, помещаемый в /.well-known/security.txt на веб-сайте, содержащий контактную информацию для исследователей безопасности, чтобы сообщить об уязвимостях. Помогает скоординировать ответственное разглашение.
Развёрнутый ответ
Формат согласно RFC 9116:
Contact: mailto:security@example.com
Contact: https://example.com/security-report
Expires: 2025-12-31T23:59:59Z
Encryption: https://example.com/pgp-key.txt
Preferred-Languages: en, ru
Canonical: https://example.com/.well-known/security.txt
Policy: https://example.com/vulnerability-disclosure-policy
Размещение файла улучшает процесс обнаружения контактов и демонстрирует зрелость программы безопасности. В Go нужно просто раздать статический файл:
http.Handle("/.well-known/", http.StripPrefix("/.well-known/", http.FileServer(http.Dir("./well-known"))))
Файл должен быть доступен по HTTPS. Некоторые организации добавляют цифровую подпись для проверки аутентичности.
Под капотом (internals)
security.txt не требует специального парсинга — это plain text. Важно устанавливать заголовки Content-Type: text/plain и Cache-Control: max-age=3600, но не слишком агрессивный кеш из-за даты Expires. Некоторые сканеры проверяют наличие файла для быстрого поиска контактов.
Типичные ошибки кандидатов
- Не обновлять поле
Expires
Просроченный файл сигнализирует, что программа безопасности не поддерживается, исследователи уйдут. Нужен cron/CI для обновления. - Размещение файла без HTTPS
Соединение не защищено, злоумышленник может подменить контакты и перенаправлять сообщения на свой адрес. - Слишком жёсткое ограничение
Policy(угрозы судебных исков)
Отпугивает добросовестных исследователей. Политика должна быть ясной и благодарственной.
Возможные дополнительные вопросы интервьюера
Обязательно ли размещать PGP ключ?
Не обязательно, но рекомендуется для шифрования чувствительных отчётов. Увеличивает доверие и защищает исследователя при передаче деталей уязвимости.
Как проверить корректность security.txt?
Существуют онлайн-валидаторы (securitytxt.org) и линтеры, которые проверяют формат, HTTPS и обязательные поля.
Можно ли автоматизировать публикацию в Go-веб-приложении?
Да, через генерацию файла на лету с динамической датой Expires, используя шаблоны и кеширование, но проще статическим файлом с автообновлением через CI/CD.
Как работает crypto/tls и как настроить безопасные cipher suites?
Краткий ответ (30–60 секунд)
Пакет crypto/tls Go реализует протокол TLS 1.0–1.3. Безопасная настройка подразумевает минимум TLS 1.2 (предпочтительно 1.3), исключение слабых cipher suites, предпочтение AEAD (AES-GCM, ChaCha20-Poly1305) и ECDHE. В 1.3 cipher suites задаются автоматически и безопасно.
Развёрнутый ответ
Пример безопасной конфигурации сервера:
config := &tls.Config{
MinVersion: tls.VersionTLS13,
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
CipherSuites: nil, // для 1.3 не используется; для 1.2 раскомментировать список безопасных
// Для TLS 1.2 можно задать:
// CipherSuites: []uint16{
// tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
// tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
// tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
// tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
// },
PreferServerCipherSuites: true, // только для 1.2, в 1.3 игнорируется
}
Клиент: также MinVersion, RootCAs и ServerName для проверки сервера.
Под капотом (internals)
В crypto/tls весь handshake закодирован в конечных автоматах: clientHandshakeState и serverHandshakeState. Для TLS 1.2 CipherSuites — список 16-битных идентификаторов; cipherSuite содержит функции hkdfExpandLabel, finishedHash, AEAD-обёртку. При активации 1.3 набор шифров выбирается из встроенного безопасного списка (defaultCipherSuitesTLS13), пользовательский CipherSuites игнорируется; можно лишь переопределить через Config.CipherSuites но не рекомендуется. Поддерживаются только AES-GCM и ChaCha20-Poly1305. crypto/rsa для подписей (PSS) и crypto/ecdsa.
Типичные ошибки кандидатов
- Добавление слабых cipher suites (RC4, 3DES, CBC) в белый список
RC4 сломан, 3DES — 64-битная стойкость, CBC без EtM — padding oracle. Go не включает их по умолчанию, но вручную можно — никогда не делайте. - Отключение проверки сертификата на клиенте (
InsecureSkipVerify)
См. предыдущие вопросы. - Установка
MinVersion: tls.VersionTLS12без ограничения cipher suites
По умолчанию Go для 1.2 включает безопасные GCM/ChaCha20 наборы, но ручные правки могут испортить. Лучше минимизировать, либо оставить пустым.
Возможные дополнительные вопросы интервьюера
Как настроить поддержку отечественных криптоалгоритмов (ГОСТ) в Go?
Стандартный crypto/tls не включает. Нужны патчи (например, gost-crypto), форки или прокси-сервер, который терминирует TLS и использует ГОСТ-алгоритмы.
Как в Go отключить TLS 1.2 и оставить только 1.3?
MinVersion: tls.VersionTLS13. Обратите внимание, что клиенты, не поддерживающие 1.3, не смогут подключиться.
Что такое Cipher Suite ordering и как он работает в Go?
При PreferServerCipherSuites: true сервер выбирает из своего списка, а не клиентского. В 1.3 выбор за сервером всегда, и он предпочитает первый поддерживаемый набор из своих настроек.
Что такое "HTTP Strict Transport Security" (HSTS)?
Краткий ответ (30–60 секунд)
HSTS — веб-политика, заставляющая браузеры всегда обращаться к сайту только по HTTPS, даже если пользователь вводит http://. Сервер передаёт заголовок Strict-Transport-Security. Устраняет downgrade-атаки и cookie hijacking. В Go добавляется как заголовок ответа.
Развёрнутый ответ
Заголовок:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
max-age— срок действия в секундах (рекомендуется 1 год).includeSubDomains— распространяет на поддомены.preload— запрос на включение в preload list браузеров (после регистрации на hstspreload.org).
Реализация middleware в Go:
func HSTS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil {
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload")
}
next.ServeHTTP(w, r)
})
}
Под капотом (internals)
Браузер при получении заголовка для данного хоста заносит его в базу HSTS (встроенный список). Последующие запросы на http:// автоматически заменяются на https://, даже до отправки первого пакета. preload позволяет включить домен в код браузера (Chromium, Firefox), что защищает и самый первый визит. Опасность: если HTTPS не настроен правильно, можно «залочить» пользователей вне доступа, поэтому предзагрузка требует осторожности. Go сервер лишь добавляет заголовок, браузер выполняет всю логику.
Типичные ошибки кандидатов
- Отправка HSTS заголовка по HTTP
Браузер игнорирует HSTS заголовки, полученные по незащищённому соединению. Заголовок должен выдаваться только при ответе через HTTPS. - Слишком маленький
max-age(несколько дней)
Уменьшает окно защиты. Рекомендуется минимум 1 год (31536000), перед preload 2 года. - Включение
preloadбез полной готовности HTTPS на всех поддоменах
Можно необратимо закрыть доступ к поддоменам, если там нет HTTPS. Начинать сmax-ageмалого, тестировать, затем увеличивать.
Возможные дополнительные вопросы интервьюера
Можно ли отозвать HSTS после preload?
Удаление из preload листов долгий процесс (месяцы). Перед preload нужно убедиться в 100% покрытии HTTPS.
Как HSTS защищает от SSL stripping?
Атакующий не сможет перехватить первый HTTP запрос, потому что браузер уже знает, что сайт использует только HTTPS. Без HSTS возможна атака, устраняющая перенаправление на HTTPS.
Как в Go протестировать HSTS middleware?
В тестах создать httptest.NewServer с TLS и проверить, что в ответе есть заголовок Strict-Transport-Security, задав TLS-соединение.
Что такое "clickjacking" и как защититься?
Краткий ответ (30–60 секунд)
Clickjacking — атака, при которой пользователь кликает по видимым элементам интерфейса, но на самом деле прозрачные или внедрённые фреймы направляют клик на скрытый целевой сайт. Защита в Go: HTTP-заголовки X-Frame-Options (DENY/SAMEORIGIN) и Content-Security-Policy с frame-ancestors.
Развёрнутый ответ
Пример атаки: злоумышленник встраивает сайт банка в прозрачный iframe поверх своей страницы. Пользователь думает, что кликает на «выиграть приз», а на самом деле подтверждает перевод денег. Защита:
func noClickjack(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Frame-Options", "DENY") // или SAMEORIGIN
// более современно:
w.Header().Set("Content-Security-Policy", "frame-ancestors 'none'")
next.ServeHTTP(w, r)
})
}
X-Frame-Options— старый, но широко поддерживаемый.frame-ancestorsв CSP — более гибкий: можно указать конкретные источники.
Под капотом (internals)
Браузер при загрузке страницы во фрейме проверяет X-Frame-Options или директиву frame-ancestors родительского документа. Если запрещено, фрейм не отображается, или браузер отказывается загружать. Множественные значения (несколько заголовков) могут привести к противоречиям, поэтому рекомендуется использовать CSP.
Типичные ошибки кандидатов
- Защита только
X-Frame-Optionsбез CSP
Некоторые браузеры могут игнорировать или неправильно интерпретировать комбинации. Используйтеframe-ancestorsдля современных. - Разрешение
SAMEORIGINбез учёта возможности XSS на поддоменах
XSS на поддомене позволит встроить фрейм. ЛучшеDENYесли это не требуется. - Применение заголовка не на всех страницах, особенно чувствительных
Middleware должно применяться глобально.
Возможные дополнительные вопросы интервьюера
Как защитить сайт от двойной клик-атаки без JavaScript?
Использовать frame-ancestors 'none' и X-Frame-Options: DENY. Дополнительно серверные меры: проверка реферера, одноразовые токены для критических действий.
Чем отличается clickjacking от likejacking?
Likejacking — частный случай, когда злоумышленник скрывает под кнопкой «Like» социальную кнопку, накручивая лайки.
Как отладка CSP может помочь выявить попытки clickjacking?
Установка Content-Security-Policy-Report-Only с frame-ancestors 'none'; report-uri /csp-report позволяет собирать отчёты, не блокируя, и выявлять попытки встраивания.
Что такое "темпоральные данные" и как их безопасно передавать?
Краткий ответ (30–60 секунд)
Темпоральные данные (сессионные токены, одноразовые коды, токены сброса пароля) имеют ограниченное время жизни. Их безопасная передача требует использования HTTPS, короткого TTL, одноразовости (если применимо), передачи через безопасные каналы (email/SMS с осторожностью) и проверки владения каналом.
Развёрнутый ответ
Примеры: токены в письме для сброса пароля, одноразовые ссылки для подтверждения email, OTP-коды. Проблемы: перехват по HTTP, утечка через referrer, хранение в истории браузера. Подходы:
- HTTPS обязательно, с HSTS.
- Короткое время жизни — токен должен истечь через несколько минут или часов.
- Одноразовость — пометка использованного токена в БД/кеше.
- Передача через redirect с использованием POST/клиентских заголовков, исключение токенов из URL (чтобы не попадать в логи, referrer). Для сброса пароля лучше вместо прямой ссылки отправлять код, который пользователь вводит на странице.
- Безопасное хранение на сервере: хеширование токена в БД.
- Проверка канала: подтверждение через код, отправленный на известный email/SMS, а не через внешние сервисы.
В Go генерация безопасного токена:
tokenBytes := make([]byte, 32)
rand.Read(tokenBytes)
token := base64.URLEncoding.EncodeToString(tokenBytes)
hashedToken := sha256.Sum256(tokenBytes) // хранить хеш
Под капотом (internals)
Стандартный подход: после генерации токена, сервер сохраняет в redis запись reset:<hashed> -> userID с TTL. При использовании сверяет хеш, удаляет ключ. Использование crypto/rand гарантирует непредсказуемость. Никогда не используйте предсказуемые временные метки или userID в качестве токена.
Типичные ошибки кандидатов
- Передача токена сброса в URL и сохранение его в логах сервера
Веб-сервер логирует URL. Токен утекает. Лучше двухшаговый сброс: сначала отправляем код, пользователь вводит его на форме. - Отсутствие TTL у токена
Токен может быть использован через год при утечке БД. Всегда устанавливайте абсолютный таймаут. - Проверка токена без постоянной временной сложности
Если сравнение хеша через==, возможна timing-атака. Используйтеsubtle.ConstantTimeCompare.
Возможные дополнительные вопросы интервьюера
Как реализовать «magic link» безопасно?
Генерировать одноразовый токен, хранить его хеш с TTL. Отправлять ссылку вида /login?token=.... При переходе проверять токен, менять статус на использованный, сразу устанавливать сессионную cookie. Обязательно HTTPS.
Почему опасно отдавать access token в URL для SPA?
Может попасть в реферер-заголовки при уходе на внешний сайт, в историю браузера и логи сервера. Лучше передавать через redirect с #hash или через POST и куки.
Как защитить одноразовый код от брутфорса?
Rate limiting на ввод кода, увеличение задержки после нескольких неверных попыток, блокировка после лимита.
Как работает gosec и как он помогает писать безопасный код?
Краткий ответ (30–60 секунд)
gosec — статический анализатор безопасности для Go, который сканирует исходный код на наличие распространённых уязвимостей: hardcoded credentials, SQL injection, использование слабых криптографических алгоритмов, небезопасные практики работы с TLS. Интегрируется в CI/CD, помогает выявлять проблемы на ранних этапах.
Развёрнутый ответ
gosec имеет множество правил (-include/-exclude):
G101— hardcoded credentials.G103— unsafe block usage (audit).G104— audit: check errors not checked.G106— audit: use of ssh InsecureIgnoreHostKey.G201— SQL query construction using formatting.G401— использование слабых хеш-функций (MD5, SHA1).G501— импортcrypto/md5.G601— range statement с ошибкой копирования (небезопасно).
Запуск: gosec ./.... Интеграция с golangci-lint также включает gosec как линтер. Пример конфига .gosec.json. В Go-проектах помогает enforcing лучших практик без ручного ревью.
Под капотом (internals)
gosec построен на go/ast и go/ssa, анализирует AST дерево на наличие опасных шаблонов. Например, проверяет, содержит ли бинарное выражение в вызове db.Query оператор конкатенации. Имеет call graph, может отслеживать переменные. Сканирует конфигурационные файлы (.yaml). Работает быстро, поскольку анализирует только выбранные пакеты.
Типичные ошибки кандидатов
- Игнорирование предупреждений
gosecбез разбора
Многие настраивают исключение правил, не понимая суть. Лучше точечно подавлять false positives комментарием/* #nosec G104 */. - Отсутствие интеграции в CI
Проверка только локально не даёт гарантий.gosecдолжен быть частью пайплайна, fail build при критических ошибках. - Полагание, что
gosecпокрывает все уязвимости
Статический анализ не может заменить динамические тесты и пентесты.gosec— первый рубеж, а не серебряная пуля.
Возможные дополнительные вопросы интервьюера
Как добавить своё правило в gosec?
Реализовать интерфейс gosec.Rule с методами ID(), Match(ast.Node, *gosec.Context) (*issue, error). Можно создать плагин и подключить через -custom.
Что такое #nosec комментарий и когда его использовать?
#nosec отключает проверку для конкретной строки; используется, если уверены в безопасности (например, тестовые данные). Злоупотребление недопустимо, требует ревью.
Может ли gosec найти уязвимости в зависимостях?
Нет, gosec сканирует только ваш код. Для зависимостей используют govulncheck или trivy.
Что такое "принцип наименьших привилегий" (PoLP)?
Краткий ответ (30–60 секунд)
Принцип наименьших привилегий гласит, что каждый субъект (пользователь, процесс, сервис) должен обладать только минимально необходимыми правами для выполнения своих задач. Снижает ущерб от компрометации и ошибки. В Go это проявляется в разделении сервисов, ограничении прав БД, использовании отдельных учётных записей.
Развёрнутый ответ
Применение в Go-окружении:
- Пользователь БД: приложение подключается к БД под пользователем, имеющим доступ только к нужным таблицам и операциям (SELECT, INSERT, но не DROP).
- Системный пользователь: Go-процесс запускается под непривилегированным UID (не root). В контейнерах
USER 1000. - RBAC на уровне API: middleware авторизации, дающее доступ только к разрешённым эндпоинтам.
- Сеть: egress-фильтры разрешают только нужные адреса/порты (SSRF защита).
- Файловая система: доступ только к необходимым директориям (read-only там, где возможно).
- Capabilities в Linux: ограничение
CAP_NET_BIND_SERVICEдля привязки к низким портам, но не процессам.
Пример Dockerfile:
USER 1000:1000
В коде Go: проверка, что файлы конфигураций не пишутся (os.O_RDONLY).
Под капотом (internals)
При старте Go-процесс наследует UID/GID от родителя. С помощью syscall.Setuid можно сменить пользователя, но после получения capabilities. В Kubernetes через securityContext: runAsNonRoot: true. Привилегии управляются на уровне ОС и контейнера, а не языка. Однако в Go можно настроить net.Listener через syscall.SetsockoptInt для SO_REUSEADDR без root.
Типичные ошибки кандидатов
- Запуск Go-сервера от root
Если процесс скомпрометирован, атакующий получает root. Всегда запускать под выделенным пользователем. - Использование одного мощного пользователя БД для всех сервисов
Компрометация одного сервиса приводит к полной утере данных всех. Каждому микросервису свой аккаунт. - Чрезмерно открытые CORS и сетевые политики
*разрешение всех origins даёт излишние привилегии. Следует ограничивать конкретными доверенными доменами.
Возможные дополнительные вопросы интервьюера
Как PoLP связан с Zero Trust?
Zero Trust расширяет принцип наименьших привилегий на сетевое взаимодействие: ни одна сущность не доверяется по умолчанию, даже внутри периметра. Каждый запрос должен аутентифицироваться и авторизовываться.
Как в Go реализовать runtime понижение привилегий?
После старта и бинда порта (если нужен <1024) процесс может выполнить syscall.Setuid(uid) и syscall.Setgid(gid), отказавшись от root.
Достаточно ли только файрволов для соблюдения PoLP на сетевом уровне?
Нет, нужна также авторизация на уровне приложения (mTLS, токены), чтобы даже внутри сети сервис не мог вызывать неразрешённые API.
Как хранить пароли в базе данных (best practices)?
Краткий ответ (30–60 секунд)
Пароли никогда не хранить в открытом виде или с обратимым шифрованием. Использовать специализированные функции: bcrypt, Argon2id. Каждый пароль солируется случайной уникальной солью, применяется высокая вычислительная стоимость. Хранить результирующую строку (включая идентификатор алгоритма, стоимость, соль и хеш) в базе.
Развёрнутый ответ
Процесс в Go:
-
При регистрации:
Go password := []byte(userPassword) hash, err := argon2id.CreateHash(password, argon2id.DefaultParams) // гипотетическая обёртка // или bcrypt hash, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) -
Сохранить
hashв БД (поле VARCHAR 255). -
При входе:
Go err = bcrypt.CompareHashAndPassword(storedHash, password)
Дополнительные меры:
- Применять pepper (секретный ключ приложения), добавляемый к паролю перед хешированием, хранящийся в Vault.
- Включить политику сложности паролей (минимальная длина).
- Для миграции старых хешей: обернуть старый SHA-хеш в bcrypt (перехеширование).
- Никогда не логировать пароль.
Под капотом (internals)
Bcrypt и Argon2 выходная строка стандартизирована: $алгоритм$параметры$соль$хеш. Библиотека bcrypt в Go использует crypto/subtle для сравнения и не выдаёт пароль в ошибках. При создании хеша с cost=10 выполняется 2^10 раундов расширения ключа.
Типичные ошибки кандидатов
- Хранение паролей в виде SHA-256(salt+password)
Легко брутфорсится. Только адаптивные функции. - Использование одного pepper без соли
Одинокий pepper без уникальной соли всё ещё уязвим к радужным таблицам в пределах pepper. Соль обязательна. - Ограничение длины пароля без причины (например, 20 символов)
Пользователи должны иметь возможность использовать длинные пароли / менеджеры паролей. Хеш-функции принимают длинные входы; максимум можно установить для предотвращения DoS (например, 512 символов).
Возможные дополнительные вопросы интервьюера
Как мигрировать с MD5 на bcrypt без потери доступа пользователей?
При успешной аутентификации через старый MD5, берёте введённый пароль, генерируете bcrypt хеш и заменяете. Таким образом, постепенно все активные пользователи обновят хеш.
Что делать, если алгоритм хеширования устарел (например, переходим с bcrypt на Argon2id)?
Аналогично: при успешной проверке старого хеша создаём новый Argon2 хеш и сохраняем. Поддерживаем оба алгоритма на время миграции.
Нужно ли скрывать факт, что пользователь существует?
В идеале сообщения при логине «неверный email или пароль», без различия для защиты от перечисления. Время ответа должно быть одинаковым для существующих и несуществующих пользователей.
Что такое "zero trust" архитектура?
Краткий ответ (30–60 секунд)
Zero Trust — модель безопасности, при которой ни одна сущность (пользователь, устройство, сетевой сегмент) не является доверенной по умолчанию, даже внутри периметра. Каждый запрос должен быть явно аутентифицирован и авторизован, часто с микросегментацией, mTLS и непрерывной проверкой состояния.
Развёрнутый ответ
Ключевые принципы:
- Never trust, always verify: проверять идентичность и права при каждом запросе.
- Минимальные привилегии: доступ только к конкретным ресурсам и операциям.
- Микросегментация: сервисы изолированы, даже внутри одного кластера.
- Шифрование везде: mTLS между сервисами.
- Непрерывный мониторинг: логи, поведенческий анализ, детектирование аномалий.
В Go-разработке внедряется через:
- mTLS между микросервисами (сервисная сетка Istio, Linkerd или ручной TLS с проверкой клиентских сертификатов).
- Аутентификацию и авторизацию на уровне каждого сервиса, без сквозного доверия через внутреннюю сеть.
- Использование JWT с детальными claims и постоянной проверкой.
- API-шлюз, который проверяет каждый запрос перед передачей.
Под капотом (internals)
В Go для выполнения нулевого доверия применяют sidecar-прокси (Envoy), которые терминируют mTLS от приложений. Соединения между sidecar'ами происходят с эфемерными ключами. В Go коде можно использовать crypto/tls с RequireAndVerifyClientCert и список доверенных CA. Для авторизации часто интегрируют Open Policy Agent (OPA) через middleware.
Типичные ошибки кандидатов
- Полагание, что внутренняя сеть безопасна, и отключение mTLS между сервисами
Компрометация одного контейнера даёт возможность атаковать другие. mTLS обязателен. - Один единственный токен на всю систему без учёта контекста
Access токен должен быть ограничен scope, audience и временем. Не использовать универсальный супертокен. - Отсутствие идентификации сервиса (service identity)
Сервисы должны аутентифицироваться друг перед другом не только по IP, но и по сертификатам или токенам SPIRE/SPIFFE.
Возможные дополнительные вопросы интервьюера
Что такое SPIFFE и как он помогает в Zero Trust?
SPIFFE выдаёт стандартизированные удостоверения (spiffe://domain/service) для сервисов. В Go библиотека spiffe/go-spiffe/v2 позволяет получать X.509 сертификаты и использовать их в mTLS.
Как совместить Zero Trust с CI/CD?
Доставка обновлений должна быть подписана, развёртывание только из доверенных источников, проверка целостности артефактов. Никакой сервис не должен деплоиться без верификации.
Влияет ли Zero Trust на производительность?
mTLS добавляет накладные расходы на handshake, но современные алгоритмы и session resumption снижают их. Сервисные сетки (istio) оптимизируют это.
Как работает webhook signature verification?
Краткий ответ (30–60 секунд)
Webhook signature verification — метод подтверждения, что входящий webhook действительно отправлен заявленным отправителем, а не подделан. Обычно используется HMAC с общим секретом: отправитель подписывает тело запроса, получатель проверяет подпись. В Go реализуется через вычисление HMAC-SHA256 и сравнение с заголовком X-Hub-Signature-256.
Развёрнутый ответ
Пример для GitHub webhooks:
- GitHub вычисляет
HMAC-SHA256(body, secret), отправляет заголовокX-Hub-Signature-256: sha256=<hash>. - Получатель (Go-сервер) делает то же самое и сверяет хеши.
func verifySignature(secret []byte, signatureHeader string, body []byte) bool {
if !strings.HasPrefix(signatureHeader, "sha256=") {
return false
}
expectedMAC := hex.EncodeToString(computeHMAC(secret, body))
receivedMAC := signatureHeader[7:]
return hmac.Equal([]byte(expectedMAC), []byte(receivedMAC))
}
func computeHMAC(key, message []byte) []byte {
mac := hmac.New(sha256.New, key)
mac.Write(message)
return mac.Sum(nil)
}
Важно использовать hmac.Equal для сравнения constant-time.
Под капотом (internals)
crypto/hmac реализует стандарт RFC 2104. Внутренне использует двойное хеширование. hmac.Equal — алиас к subtle.ConstantTimeCompare. При проверке подписи необходимо сначала вычислить HMAC корректной длины, а затем сравнить.
Типичные ошибки кандидатов
- Сравнение строк через
==илиstrings.EqualFold
Подвергает timing-атакам, позволяя злоумышленнику подобрать валидный хеш по времени отклика. Толькоhmac.Equal. - Чтение тела запроса несколько раз без сохранения
http.Request.Bodyможно прочитать один раз. Нужно сохранитьbodyBytes, _ := io.ReadAll(r.Body); r.Body = io.NopCloser(bytes.NewReader(bodyBytes))перед проверкой. - Пренебрежение проверкой временных меток (timestamp) в подписанных данных
Некоторые webhook-сервисы (например, Stripe) включают timestamp для защиты от replay. Проверяйте, что разница во времени не превышает допуск (например, 5 минут).
Возможные дополнительные вопросы интервьюера
Как организовать безопасное хранение webhook секрета?
В переменных окружения или Vault. Не коммитить. Секрет должен быть разным для каждой интеграции.
Зачем некоторые сервисы кроме подписи добавляют t=timestamp?
Чтобы предотвратить replay-атаки. Если подпись включает timestamp, то старый корректный запрос не будет принят после окна допуска.
Что делать, если нужно поддерживать несколько версий секрета для ротации?
Сторона отправителя может использовать текущий секрет, а получатель проверяет ключ по идентификатору ключа (kid) или пробует несколько активных секретов.
Что такое "CSRF токен" и как его реализовать?
Краткий ответ (30–60 секунд)
CSRF токен — случайное значение, привязанное к сессии пользователя, которое включается в формы и запросы на изменение состояния. Сервер проверяет его при получении запроса, предотвращая межсайтовую подделку. В Go реализуется через генерацию токена для сессии, вставку в форму/заголовок и проверку на сервере.
Развёрнутый ответ
Подробная реализация:
- При создании сессии генерируется случайный токен (32 байта) и сохраняется в сессии.
-
Токен вставляется в HTML-форму как скрытое поле:
HTML <input type="hidden" name="csrf_token" value="{{.CSRF}}"> -
Также может передаваться в JavaScript через
<meta name="csrf">и добавляться в заголовокX-CSRF-Tokenдля AJAX. - Middleware проверяет не-GET запросы: извлекает токен из параметров или заголовка и сравнивает с сохранённым в сессии через constant-time.
Пример middleware:
func CSRFCheck(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" || r.Method == "HEAD" {
next.ServeHTTP(w, r); return
}
token := r.Header.Get("X-CSRF-Token")
if token == "" {
token = r.FormValue("csrf_token")
}
sessionToken := getSessionCSRFToken(r)
if subtle.ConstantTimeCompare([]byte(token), []byte(sessionToken)) != 1 {
http.Error(w, "Forbidden", 403); return
}
next.ServeHTTP(w, r)
})
}
Под капотом (internals)
В Go gorilla/csrf использует маскированный токен (masked token) — объединение случайного токена и его HMAC от сессии, чтобы не хранить отдельно на сервере. При проверке разбивается и сверяется HMAC. Позволяет stateless CSRF защиту. Токен меняется после каждого запроса (Double Submit Cookie) для дополнительной защиты.
Типичные ошибки кандидатов
- Использование предсказуемого токена (на основе времени)
Должен быть криптостойким. - Передача CSRF токена в URL
URL попадает в логи, referer, историю. Только тело запроса или заголовок. - Отсутствие привязки токена к сессии
Если токен не привязан к конкретному пользователю, можно украсть токен у одного и применить к другому.
Возможные дополнительные вопросы интервьюера
Чем отличается Double Submit Cookie от синхронного токена?
Double Submit Cookie: токен в куке + в заголовке; сервер ожидает, что они совпадут. Не требует серверного состояния, но уязвим при наличии поддоменных кук. Синхронный токен хранится на сервере и сверяется с пришедшим.
Как интегрально реализовать CSRF защиту для SPA на Go бекенде?
Использовать SameSite=Strict/Lax Cookie для сессии + кастомный заголовок X-CSRF-Token, проверяемый на бекенде. Токен может быть прочитан из HttpOnly cookie? Нет, HttpOnly не доступен JS. Поэтому SPA часто используют кастомный заголовок с токеном из localStorage, что рискованно. Лучше использовать SameSite=Strict и проверку Origin.
Нужно ли защищать API, использующее только JSON и Bearer токен?
Если токен передаётся через заголовок Authorization, CSRF не работает, так как браузер не добавляет этот заголовок автоматически. Главное — не разрешать использование кук для аутентификации.
Как предотвратить "DoS" атаки с помощью rate limiting?
Краткий ответ (30–60 секунд)
Rate limiting ограничивает количество запросов от одного клиента за промежуток времени. В Go применяют пакет golang.org/x/time/rate (token bucket) или middleware, отслеживающее IP/токен в памяти или Redis. Добавляют заголовки Retry-After, X-RateLimit-*, возвращают 429 Too Many Requests.
Развёрнутый ответ
Пример простого middleware (in-memory):
var limiterMap sync.Map
func rateLimit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
limiterAny, _ := limiterMap.LoadOrStore(ip, rate.NewLimiter(10, 20))
limiter := limiterAny.(*rate.Limiter)
if !limiter.Allow() {
w.Header().Set("Retry-After", "1")
http.Error(w, "Too Many Requests", 429)
return
}
next.ServeHTTP(w, r)
})
}
Для распределённых систем счётчики хранят в Redis (алгоритм скользящего окна или token bucket с Lua), чтобы обеспечить глобальную согласованность.
Под капотом (internals)
x/time/rate хранит последнее время обновления токенов и их количество. Allow() вычисляет, можно ли взять токен сейчас. Потокобезопасен. В Redis часто применяют INCR + EXPIRE с запросом на Lua-скрипт для атомарности. Более сложный алгоритм Generic Cell Rate Algorithm (GCRA) — основа многих корпоративных систем.
Типичные ошибки кандидатов
- Игнорирование X-Forwarded-For при обратном прокси
RemoteAddrбудет IP прокси, лимитирование по нему заблокирует всех. Нужно доверятьX-Forwarded-Forс проверкой. - Не различать rate limit на разные эндпоинты
Дорогие операции должны иметь более строгие лимиты. Один глобальный лимит неэффективен. - Отсутствие мониторинга и алертов на rate-limiting
При атаке важно знать, что срабатывает лимитер, чтобы реагировать.
Возможные дополнительные вопросы интервьюера
Как реализовать приоритетный rate limiting (VIP пользователи)?
Для разных категорий пользователей создавать отдельные лимитеры с разными параметрами, определяя категорию по токену/API ключу.
В чём отличие rate limiting от throttling?
Rate limiting — жёсткое ограничение, превышающие запросы отклоняются. Throttling — замедление пропускной способности без отбрасывания (например, очередь).
Как бороться с обходом rate limiting через смену IP (ботнет)?
Использовать техники капчи, fingerprinting, анализировать поведение. Rate limiting — это лишь часть защиты.
Что такое "regexp injection" (ReDoS) и как ее избежать?
Краткий ответ (30–60 секунд)
ReDoS — атака, эксплуатирующая алгоритмическую сложность некоторых регулярных выражений, приводящую к экспоненциальному времени обработки специально подготовленного ввода. В Go стандартный regexp использует алгоритм с гарантированной линейной сложностью, но уязвимость может проявиться при использовании множественных жадных квантификаторов внутри друг друга.
Развёрнутый ответ
Пример опасного выражения: ^(a+)+$. Вход "aaaaaaaaX" заставляет backtracking машину перебирать огромное число комбинаций. Go regexp реализует алгоритм RE2 (форк от Google), который не использует backtracking для основных операций; он строит NFA и выполняет за O(n) время. Однако есть нюанс: использование re.FindAllString с большими строками или наличие \b и т.п. может вызывать повышенные затраты. Но чисто backtracking отсутствует, так что классический ReDoS в Go практически невозможен. Тем не менее, не стоит злоупотреблять сложными выражениями без ограничений.
В Go защита:
- Проверять timeout для операций, если входной размер неконтролируем.
- Использовать
regexp.MustCompileи анализировать прохождение. - Избегать выражений с чередованием и вложенными квантификаторами, даже если движок RE2 справляется лучше, чем PCRE.
Под капотом (internals)
regexp в Go реализует машину Томпсона (NFA). Компиляция превращает выражение в программу инструкций. При сопоставлении она симулирует одновременное нахождение в нескольких состояниях, что гарантирует линейное время от длины ввода. Это исключает экспоненциальные откаты. Единственное, что может замедлить — большое количество состояний NFA (очень сложное выражение), приводящее к большим накладным расходам на шаг, но всё равно линейно.
Типичные ошибки кандидатов
- Использование
re2/regexpс ожиданием backtracking семантики (типа backreferences)
Goregexpне поддерживает backreferences (они NP-полны). Многие переносят выражения из PCRE без адаптации, получая ошибки компиляции или другое поведение. - Неиспользование
regexp.QuoteMetaдля экранирования пользовательского ввода при вставке в regex
При динамической вставке строки в regex можно случайно получить нежелательные метасимволы; всегда экранировать. - Слепое доверие к линейной сложности и игнорирование ресурсов памяти
Очень большая строка + сложный regex могут потреблять много памяти/времени на обработку всех совпадений. Надо задавать ограничения на размер ввода.
Возможные дополнительные вопросы интервьюера
Чем отличается движок Go regexp от PCRE?
Go regexp базируется на конечных автоматах, линейное время, но нет backreferences, look-behind, поддержки рекурсии. PCRE мощнее, но потенциально экспоненциален.
Как ограничить длительность выполнения регулярного выражения в Go?
Можно запустить сопоставление в отдельной горутине с контекстом и таймаутом, используя context.WithTimeout и убивать горутину (хотя отмена горутины не проста). Лучше контролировать длину входа.
Что такое catastrophic backtracking и почему он не страшен в Go?
Это патологический случай backtracking PCRE, где количество попыток экспоненциально растёт. Go regexp не имеет backtracking кроме некоторых ограниченных случаев (по стандарту RE2). Так что защищён.
Как безопасно парсить пользовательский ввод?
Краткий ответ (30–60 секунд)
Безопасный парсинг пользовательского ввода включает строгую валидацию по белому списку, нормализацию, использование безопасных API для контекста (SQL — параметризация, HTML — шаблонизация, команды — без shell). В Go также важно ограничивать размер ввода, проверять типы, не использовать unsafe преобразования без необходимости.
Развёрнутый ответ
Общие принципы:
- Валидация до обработки: определяем, что является допустимым (регулярное выражение по белому списку), отклоняем несоответствующее.
- Нормализация: Unicode NFC нормализация (
golang.org/x/text/unicode/norm) для предотвращения обхода фильтров. - Экранирование в зависимости от контекста вывода:
- SQL: только параметризованные запросы.
- HTML:
html/template. - Команды ОС: передача аргументов без shell, использование
exec.Commandс отдельными параметрами. - URL:
url.QueryEscape,net/urlдля парсинга.
- Ограничение размера:
io.LimitReader,http.MaxBytesReader. - Проверка типов:
strconv.Atoiвместоfmt.Sscanfдля чисел. - Избегать
reflectиunsafeпри обработке пользовательского ввода.
Пример безопасного чтения целого числа:
id, err := strconv.Atoi(input)
if err != nil || id < 0 {
http.Error(w, "bad request", 400)
return
}
Под капотом (internals)
Пакет strconv проводит разбор без eval. xml, json декодеры обрабатывают структурированные данные безопасно (при корректном использовании). html/template автоматически выбирает функцию экранирования в зависимости от контекста в шаблоне. x/text/transform помогает нормализовать потоки.
Типичные ошибки кандидатов
- Чрезмерное доверие к клиентской валидации
Вся клиентская валидация должна дублироваться на сервере, иначе её легко обойти. - Использование
Sscanfдля парсинга чисел из пользовательского ввода
Может быть подвержено переполнениям; лучшеstrconv. - Игнорирование многобайтовых символов и Unicode-трюков
Например, символы, похожие на/(%C0%AF), могут обойти path traversal. Всегда нормализовать и проверять после декодирования.
Возможные дополнительные вопросы интервьюера
Как обрабатывать Unicode-строки для предотвращения инъекций?
Нормализовать до NFC и проверять на наличие опасных категорий символов (\p{C}, управляющие). Go unicode позволяет отфильтровать.
Что такое двойное декодирование и как его избежать?
Если сервер декодирует URL дважды, атакующий может обойти проверки. Нужно централизованно декодировать один раз, либо использовать rawurl в зависимости от контекста.
Как в Go безопасно парсить XML?
xml.NewDecoder с установленными Strict: true, запрет внешних сущностей (по умолчанию DefaultHandler не обрабатывает), лимитирование размера.
Что такое "supply chain attack" и как Go модули помогают защититься?
Краткий ответ (30–60 секунд)
Supply chain attack — компрометация цепочки поставок ПО, когда злоумышленник внедряет вредоносный код в зависимости или инструменты сборки. В Go модули (go.mod, go.sum) и чек-сумма база данных (sum.golang.org) гарантируют целостность и неизменность зависимостей, предотвращая подмену пакетов.
Развёрнутый ответ
Виды атак: подделка репозиториев, typo-squatting, компрометация аккаунтов разработчиков, вредоносные обновления. Защита Go:
- Минимальные версии (
go.modфиксирует версии). go.sumхранит криптографические хеши каждого модуля и файлов.- Прозрачный лог (checksum database): при скачивании модуля Go проверяет хеши через сервер
sum.golang.org(там лог на основе Trillian). Если хеш не совпадает, скачивание блокируется. GONOSUMCHECKпозволяет исключать приватные модули.- Module proxy (
proxy.golang.org) — кеширующий прокси, защищающий от удаления модуля. go mod verifyпроверяет, что локальные файлы соответствуютgo.sum.- Vendoring: включение зависимостей в VCS для полного контроля.
Под капотом (internals)
Когда go get скачивает модуль, он обращается к GOPROXY (по умолчанию proxy.golang.org|direct). Хеш из go.sum сравнивается с хешами, полученными из checksum database (или GOSUMDB). Эта база использует Trillian — публичный append-only лог. Если модуль не найден в базе, Go проверяет подпись через примечание прозрачности (transparency), и если доверие не установлено, операция прерывается. go.sum файл включает версию и хеш.
Типичные ошибки кандидатов
- Удаление
go.sumиз VCS
Это ломает гарантию повторяемых сборок и защиту от изменений.go.sumдолжен коммититься. - Использование
GONOSUMDB=*илиGONOSUMCHECK=*без необходимости
Полностью отключает проверку хешей для публичных модулей, открывая дверь атакам. - Отсутствие анализа зависимостей на уязвимости
govulncheckпомогает находить известные уязвимости в зависимостях, его следует запускать в CI.
Возможные дополнительные вопросы интервьюера
Как работает sum.golang.org и можно ли его отключить для офлайн-сборок?
Это сервер журнала прозрачности; отключается через GOSUMDB=off, либо использование GONOSUMCHECK для определённых модулей. Для офлайн-сборок используют go mod vendor и отключают сеть.
Что такое go.sum database tree и как проверяется доказательство включения?
Клиент Go проверяет доказательство включения Tile, связанного с ключом подписи GOSUMDB. Это гарантирует, что хеш был занесён в лог, а не подделан.
Как подписывать собственные модули для защиты supply chain?
Можно использовать goreleaser для подписания артефактов, но для Go модулей существует проект go-tuf (The Update Framework), обеспечивающий подпись репозиториев.
Как работает go mod verify и checksum database для защиты от подмены модулей?
Краткий ответ (30–60 секунд)
go mod verify сверяет содержимое загруженных модулей с хешами в go.sum, детектируя модификации после загрузки. Checksum database (sum.golang.org) — глобальный append-only лог хешей модулей, проверяемый при загрузке: гарантирует, что модуль не был подменён с момента публикации.
Развёрнутый ответ
go mod verify:
- Проверяет каждый модуль в кеше
GOPATH/pkg/mod. - Сравнивает хеш файлов с ожидаемыми в
go.sum. - При несовпадении выводит ошибку. Полезна для детектирования ручных изменений или повреждения кеша.
Checksum database:
- При
go getсистема отправляет запросhttps://sum.golang.org/lookup/<module>@<version>. - Получает подписанный ответ с хешем и доказательством включения в лог.
- Проверяется подпись и соответствие transparency log.
- Если проверка не проходит, модуль не принимается.
- Приватные модули (под
GONOSUMCHECK) исключаются.
Под капотом (internals)
go.sum содержит строки вида:
github.com/gorilla/mux v1.8.0 h1:...=
github.com/gorilla/mux v1.8.0/go.mod h1:...=
go mod verify использует modfetch для обхода модулей в кеше и вычисления хешей. Checksum DB основан на протоколе прозрачности с ключом, встроенным в Go (можно переопределить GOSUMDB). Клиент Go проверяет тике (tile) и включение в дерево Меркле через crypto/merkle из x/mod/sumdb.
Типичные ошибки кандидатов
- Полагание, что
go.sumгарантирует безопасность без проверки внешней DB
go.sumзащищает только от изменений после загрузки; без обновления через proxy/checksum DB можно вручную подменить локальный кеш и прописать правильный хеш. Для целостности на этапе загрузки необходима checksum database. - Изменение
go.sumвручную
Это может нарушить верификацию;go.sumдолжен обновляться автоматически командамиgo mod tidy. - Игнорирование ошибок
go mod verify
Ошибки указывают на подмену пакета, необходимо немедленно исследовать.
Возможные дополнительные вопросы интервьюера
Как настроить собственный checksum database для приватных модулей?
Можно развернуть go.sum сервер (например, Athens proxy) с поддержкой проверки хешей и установить GOSUMDB на его адрес.
Как отозвать скомпрометированный модуль?
В Go нет централизованного отзыва. Разработчик может выпустить новую версию с исправлением, а пользователям рекомендуется обновить зависимости и убрать старую версию через go mod tidy. База чексумм сохраняет хеш старой версии, но это не блокирует её использование.
Что такое direct в GOPROXY и как влияет на безопасность?
direct означает скачивание напрямую из источника, минуя прокси. Прямое соединение может быть менее безопасно (нет кеширования). Лучший вариант — прокси + checksum DB.
Что такое "form tampering" защита?
Краткий ответ (30–60 секунд)
Form tampering — атака, при которой злоумышленник изменяет значения полей формы (скрытых, выпадающих списков, цен) перед отправкой на сервер. Защита: никогда не доверять клиентским данным, проверять авторизацию и допустимость значений на сервере, использовать подписанные или зашифрованные токены состояния, валидировать целостность критических полей.
Развёрнутый ответ
Пример атаки: изменение скрытого поля price с 100 на 10. Защита:
- Не полагаться на скрытые поля для критичных данных. Хранить цену и идентификатор товара на сервере (сессия/БД), а не в форме.
- Валидация и перепроверка всех значений на сервере:
- Проверять принадлежность item_id текущему пользователю.
- Пересчитывать итоговую сумму от доверенных цен из БД.
- Подписанные данные: при необходимости передать состояние в форму, использовать HMAC для подписи клиентских данных. Пример:
state = base64(json+hmac). При получении проверять HMAC. - Анти-CSRF токен для предотвращения подделки источника запроса.
В Go: всегда получать цену из базы данных:
product := db.GetProduct(r.FormValue("product_id"))
total := product.Price * quantity
Под капотом (internals)
В Go gorilla/sessions позволяет подписывать куки, что можно использовать и для форм. securecookie генерирует подписанные и зашифрованные значения. Атака на скрытое поле — классический пример небезопасного проектирования. Серверная логика должна быть источником истины.
Типичные ошибки кандидатов
- Использование только клиентской валидации для бизнес-значений
JS валидация только для UX, не для безопасности. Всё перепроверять на сервере. - Передача полного состояния заказа через скрытые поля без подписи
Позволяет подменить все параметры. Нужно либо хранить состояние на сервере, либо подписывать. - Несоответствие прав: разрешение менять item_id на который у пользователя нет прав
Даже если данные подписаны, необходимо проверять авторизацию на конкретный товар.
Возможные дополнительные вопросы интервьюера
Как защитить многошаговую форму (wizard) от пропуска шагов?
Хранить состояние и текущий шаг на сервере (в сессии/кеше) с подписанным идентификатором, и проверять, что шаги выполняются в правильном порядке.
Что такое MAC (Message Authentication Code) на уровне формы?
Сервер вычисляет HMAC от всех критических полей с серверным секретом и вставляет в форму. При отправке пересчитывает и сравнивает. Не даёт изменить значения.
Как обрабатывать загружаемые файлы в формах для предотвращения tampering?
Проверять MIME-тип, размер, сканировать на вирусы, не доверять расширению файла. Использовать серверную проверку.
Как настроить "cookie security" флаги (HttpOnly, Secure, SameSite)?
Краткий ответ (30–60 секунд)
При установке cookie в Go задаются поля HttpOnly: true (защита от доступа JS), Secure: true (передача только по HTTPS), и SameSite (Lax/Strict) для контроля отправки в межсайтовых запросах. Это предотвращает XSS-кражу, утечку по HTTP и CSRF. Пример: http.SetCookie(w, &http.Cookie{Name: "token", Value: val, HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode}).
Развёрнутый ответ
- HttpOnly:
true— кука невидима дляdocument.cookie, снижает ущерб от XSS. - Secure:
true— кука отправляется только через HTTPS, предотвращает перехват в открытых сетях. В продакшене должно быть всегдаtrue. - SameSite:
Strict— кука не отправляется для любых межсайтовых запросов; защищает CSRF, но может сломать переходы по ссылкам (пользователь придёт неаутентифицированным).Lax— кука отправляется для top-level навигаций GET, но не для POST/iframe/API. Хороший баланс.None— отправляется всегда, требует обязательныйSecure.
- Path: сужение области видимости (например,
/api).
Пример:
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: sessionID,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
Path: "/",
MaxAge: 86400,
})
Дополнительно можно установить __Host- префикс как дополнительную защиту: Name: "__Host-session" требует Secure и Path=/.
Под капотом (internals)
Браузеры применяют атрибуты при отправке: HttpOnly исключает из JavaScript окружения; Secure требует HTTPS, иначе кука игнорируется; SameSite анализирует источник запроса. net/http в Go при парсинге Cookie заголовка не проверяет атрибуты, они управляются на клиентской стороне. Сервер должен правильно выставить заголовок Set-Cookie. При использовании прокси важно, чтобы прокси не снимал эти флаги.
Типичные ошибки кандидатов
- Установка
Secure: falseв production
Куки передаются по HTTP и могут быть украдены через MITM. Исправление: всегдаSecure: true. SameSite: Noneбез крайней необходимости
Делает куку уязвимой к CSRF. Если нужно кросс-сайтовое встраивание (например, iframe), тогдаNone, но обязательноSecure.- Не использовать
HttpOnlyдля сессионных кук
При XSS злоумышленник сразу получит сессию. ВсегдаHttpOnly: true.
Возможные дополнительные вопросы интервьюера
Почему __Host- префикс важен?
Он заставляет браузер принимать куку, только если установлены Secure, Path=/ и нет атрибута Domain. Это предотвращает подделку кук поддоменами.
Как работать с куками в микросервисной архитектуре, когда домены разные?
Для кросс-доменных запросов необходимо использовать SameSite: None; Secure, если требуется общая аутентификация, но обычно лучше использовать SSO и редиректы.
Можно ли защитить HttpOnly куку от угона через XSS, если злоумышленник всё-таки получил доступ?
HttpOnly кука всё ещё может быть перехвачена, если атакующий получит возможность выполнить запросы из контекста жертвы (XSS позволяет делать запросы к тому же домену). Поэтому главное — предотвращать XSS с помощью CSP и экранирования.
Что такое Post-Quantum TLS в Go 1.24?
flowchart LR
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
Go124["Go 1.24: crypto/mlkem & Post-Quantum TLS"]:::entry -->|"X25519MLKEM768"| KEM["Гибридный обмен ключами KEM по умолчанию в crypto/tls"]:::process
KEM --> Go126["Go 1.26: crypto/hpke"]:::accent
Go126 -->|"RFC 9180 HPKE"| Stan["Стандартизированное гибридное шифрование HPKE"]:::process
Stan --> Go127["Go 1.27: crypto/mldsa"]:::success
Go127 -->|"FIPS 204 ML-DSA"| PQSig["Постквантовые цифровые подписи в crypto/x509 и TLS handshake"]:::success
Краткий ответ (30–60 секунд)
В Go 1.24 пакет crypto/tls включил по умолчанию поддержку постквантового гибридного обмена ключами X25519MLKEM768 (стандартизированного в NIST FIPS 203, реализованного в новом пакете crypto/mlkem). Это комбинация классического эллиптического протокола Диффи-Хеллмана (X25519) и решёточного механизма инкапсуляции ключей ML-KEM-768. Механизм защищает сегодняшний зашифрованный сетевой трафик от атак класса «Store Now, Decrypt Later» («Сохрани сейчас — расшифруй потом»), когда трафик перехватывается в расчёте на появление мощных квантовых компьютеров в будущем.
Развёрнутый ответ
В классическом TLS 1.3 обмен ключами опирался на ECDHE (X25519 или NIST P-256). Однако алгоритм Шора на достаточно мощном квантовом компьютере способен взломать дискретный логарифм на эллиптических кривых за полиномиальное время.
Начиная с Go 1.24:
- При исходящем клиентском вызове (
tls.Dial) клиент по умолчанию посылает вClientHelloKey Share для гибридной группыX25519MLKEM768(ранее экспериментальный драфтX25519Kyber768Draft00был заменён стандартом FIPS 203 и удалён). - Если удалённый сервер поддерживает постквантовый KEM (например, Cloudflare, Google, современные Go-серверы), сессионный ключ выводится из комбинации двух секретов: классического X25519 и постквантового ML-KEM.
- Если сервер не поддерживает ML-KEM, происходит прозрачный fallback на стандартный X25519 без разрыва соединения.
- Управление поведением: если по причинам совместимости со старыми сетевыми экранами (middleboxes, отбрасывающими большие ClientHello > 1500 байт) требуется отключить постквантовую защиту, используется переменная окружения
GODEBUG=tlsmlkem=0.
Под капотом (internals)
- KEM (Key Encapsulation Mechanism) оперирует примитивами Encap/Decap: одна сторона инкапсулирует общий секрет с использованием открытого ключа получателя, вторая — декапсулирует его своим закрытым ключом.
- Итоговый секрет выводится через HKDF:
HKDF-Extract(shared_secret_ecdh || shared_secret_mlkem). Это гарантирует, что если даже в решёточном алгоритме ML-KEM в будущем обнаружат теоретическую математическую уязвимость, надёжность соединения останется не ниже классического стойкого X25519. - В Go 1.24 реализация
crypto/mlkem(ML-KEM-768 и ML-KEM-1024 по спецификации FIPS 203) написана на чистом Go с архитектурно-оптимизированными вычислениями, что даёт минимальные накладные расходы на CPU (< 1 мс на handshake).
Типичные ошибки кандидатов
- Считать, что постквантовый TLS требует квантового компьютера:
Постквантовая криптография (PQC) работает на обычных классических x86/ARM процессорах, но использует математические задачи (обучение с ошибками на решётках — Module Learning with Errors), которые невозможно эффективно решить ни классическими, ни квантовыми алгоритмами. - Непонимание концепции гибридного обмена (Hybrid KEM):
Кандидаты полагают, что X25519 был полностью заменён на Kyber/ML-KEM. На самом деле стандарт требует одновременного использования обоих алгоритмов для минимизации криптографических рисков.
Возможные дополнительные вопросы интервьюера
- Что такое атака «Store Now, Decrypt Later»? — Злоумышленник перехватывает и сохраняет терабайты зашифрованного HTTPS-трафика уже сегодня, чтобы через 10–15 лет, с появлением квантового компьютера, восстановить сессионные ключи и расшифровать конфиденциальные данные. Гибридный PQC KEM делает это невозможным.
- Почему пакет ClientHello стал больше? — Публичные ключи и шифртексты решёточных алгоритмов занимают ~1184 байта (против 32 байт у X25519). Go автоматически разбивает или подгоняет записи TLS Record Layer, предотвращая фрагментацию TCP.
Что такое crypto/hpke в Go 1.26?
Краткий ответ (30–60 секунд)
Пакет crypto/hpke, добавленный в Go 1.26, реализует стандарт гибридного шифрования с открытым ключом HPKE (Hybrid Public Key Encryption, RFC 9180). HPKE стандартизирует комбинацию трёх примитивов: асимметричного KEM (обмен ключами), функции деривации KDF (HKDF-SHA256) и симметричного AEAD-шифра (AES-GCM / ChaCha20-Poly1305). Это фундамент для современных защищённых веб-протоколов, включая Encrypted Client Hello (TLS ECH) и Message Layer Security (MLS).
Развёрнутый ответ
До появления RFC 9180 разработчикам приходилось вручную связывать эллиптические кривые (ECDH), хэш-функции для расширения ключа и алгоритмы шифрования с аутентификацией. Малейшая ошибка в формате Nonce, AAD (Additional Authenticated Data) или деривации приводила к критическим уязвимостям.
HPKE предоставляет строго верифицированный API с гарантией стойкости против атак на основе подобранного шифртекста (IND-CCA2):
import "crypto/hpke"
// Шифрование данных на стороне клиента
suite := hpke.NewSuite(hpke.KEM_X25519_HKDF_SHA256, hpke.KDF_HKDF_SHA256, hpke.AEAD_AES256GCM)
sender, encappedKey, err := suite.NewSender(recipientPublicKey, []byte("info-context"))
if err != nil {
log.Fatal(err)
}
ciphertext, err := sender.Seal([]byte("секретное сообщение"), []byte("метаданные"))
// На стороне получателя: открытие
receiver, err := suite.NewReceiver(suite, recipientPrivateKey, encappedKey, []byte("info-context"))
plaintext, err := receiver.Open(ciphertext, []byte("метаданные"))
Под капотом (internals)
- HPKE поддерживает 4 режима работы:
Base(анонимный отправитель),Auth(с аутентификацией открытым ключом отправителя),PSK(с общим предварительным секретом) иAuthPSK. crypto/hpkeполностью интегрирован с пулом энтропииcrypto/randи защищён от утечек по побочным каналам (constant-time execution).- В архитектуре TLS ECH именно HPKE позволяет клиенту зашифровать настоящее доменное имя (SNI) публичным ключом DNS-сервера, полностью скрывая посещаемый сайт от сетевых провайдеров и цензуры.
Возможные дополнительные вопросы интервьюера
- Чем HPKE отличается от классического PGP/GPG? — HPKE — компактный, современный криптографический протокол с защитой AEAD, фиксированным контекстом деривации, поддержкой постквантовых KEM и отсутствием устаревших шифров/форматов 90-х годов.
- Как HPKE используется в TLS ECH? — Браузер получает открытый ключ сервера через специальную DNS-запись HTTPS/SVCB. Затем с помощью
hpke.Sealзашифровывает настоящий ClientHello (с приватным SNI) внутри внешнего фиктивного ClientHello.
Что такое crypto/mldsa и Post-Quantum подписи в Go 1.27?
Краткий ответ (30–60 секунд)
Пакет crypto/mldsa, включённый в стандартную библиотеку Go 1.27, реализует федеральный стандарт NIST FIPS 204 — ML-DSA (Module-Lattice Digital Signature Algorithm, бывший Crystals-Dilithium). Это постквантовый алгоритм цифровой подписи на основе математических решёток. В Go 1.27 он бесшовно интегрирован в crypto/x509 и crypto/tls, позволяя выпускать сертификаты X.509 и проводить TLS Handshake с квантово-устойчивыми цифровыми подписями.
Развёрнутый ответ
Если KEM (Kyber / ML-KEM) защищает конфиденциальность обмена ключами от подслушивания, то ML-DSA защищает подлинность (аутентификацию) от подделки цифровых подписей. Квантовый алгоритм Шора мгновенно ломает классические подписи RSA и ECDSA (ECDSA P-256 / Ed25519).
В Go 1.27 стандартная библиотека покрывает полный спектр квантовой стойкости:
crypto/mldsa— генерация ключей, создание и проверка подписей (параметры безопасностиML-DSA-44,ML-DSA-65,ML-DSA-87).crypto/x509— разбор, сериализация и валидация цепочек сертификатов с публичными ключами ML-DSA.crypto/tls— поддержка подписиCertificateVerifyв TLS 1.3 на базе ML-DSA.
import "crypto/mldsa"
// Генерация квантово-устойчивой пары ключей (уровень стойкости ML-DSA-65 / AES-192)
priv, pub, err := mldsa.GenerateKey(rand.Reader, mldsa.ML_DSA_65)
if err != nil {
log.Fatal(err)
}
message := []byte("Приказ на транзакцию #45892")
context := []byte("banking-auth-v1")
// Создание подписи
sig, err := mldsa.Sign(rand.Reader, priv, message, context, nil)
// Проверка подписи
isValid := mldsa.Verify(pub, message, context, sig, nil)
Под капотом (internals)
- ML-DSA основан на сложности задачи Module Learning With Errors (M-LWE) и Module Short Integer Solution (M-SIS). Ни один квантовый алгоритм не даёт полиномиального ускорения против решёточных структур.
- Размер открытого ключа ML-DSA-65 составляет 1952 байта, а размер цифровой подписи — 3309 байт (в сравнении с 64 байтами у Ed25519).
- Из-за увеличенного размера сертификатов и подписей сетевой стек Go 1.27 оптимизирован под передачу Jumbo-пакетов и сжатие сертификатов (RFC 8879 Certificate Compression) для минимизации сетевых задержек.
Возможные дополнительные вопросы интервьюера
- Когда компаниям необходимо внедрять ML-DSA в production? — В первую очередь для Root CA и долгоживущих сертификатов удостоверяющих центров (10–20 лет), систем электронной подписи документов и Code Signing дистрибутивов ПО, где валидность подписи должна сохраняться десятилетиями.
- Чем ML-DSA отличается от SLH-DSA (SPHINCS+)? — ML-DSA базируется на решётках и работает быстро, имея умеренный размер ключей. SLH-DSA базируется исключительно на хэш-функциях (Stateless Hash-based Signatures, FIPS 205), имеет максимальный консервативный уровень доверия, но генерирует существенно более массивные подписи (~7–17 КБ) и медленнее в вычислениях.