Безопасность и криптография в Go
Безопасность редко ломается из-за одного "сложного" места. Чаще проблема появляется из маленьких решений: залогировали token, положили пароль в Git, использовали math/rand для session id, склеили SQL строкой, приняли файл без проверки размера.
Криптография не про то, чтобы придумать свой хитрый алгоритм. Наоборот: хорошая практика - использовать проверенные примитивы и не изобретать свои схемы.
Не пишите свою криптографию
Первое правило: не придумывайте собственное шифрование, подписи, хеширование паролей или формат токенов. Используйте стандартную библиотеку и известные пакеты.
В Go есть сильные пакеты:
crypto/rand;crypto/aes;crypto/cipher;crypto/hmac;crypto/sha256;crypto/tls;golang.org/x/crypto/bcrypt.
Но даже с правильными пакетами можно ошибиться в деталях: повторить nonce, использовать слабый источник случайности, хранить ключ рядом с данными.
Случайные числа
Для безопасности используйте crypto/rand, а не math/rand.
func randomBytes(size int) ([]byte, error) { data := make([]byte, size) if _, err := rand.Read(data); err != nil { return nil, err } return data, nil }
math/rand подходит для игр, симуляций, тестов и случайного выбора цвета. Он предсказуем, если известен seed. Для токенов, паролей, ключей и reset-ссылок это недопустимо.
Токены
Session token должен быть достаточно случайным:
func newToken() (string, error) { data := make([]byte, 32) if _, err := rand.Read(data); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(data), nil }
32 байта случайности достаточно для большинства session-token задач. Base64 превращает байты в строку, которую удобно передавать в HTTP.
Token храните как secret. Не пишите его в логи и не добавляйте в URL, если можно использовать заголовок.
Пароли
Пароли нельзя хранить в открытом виде. Их также нельзя хешировать обычным SHA-256.
Используйте password hashing алгоритм, например bcrypt:
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return err }
Проверка:
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) if err != nil { return errors.New("invalid password") }
bcrypt специально медленный. Это хорошо: если база утекла, атакующему сложнее перебрать миллионы паролей.
AES-GCM
Если нужно шифровать данные симметричным ключом, используйте AEAD-режим, например AES-GCM. Он дает и конфиденциальность, и проверку целостности.
block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } nonce := make([]byte, gcm.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, err } ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
Расшифровка:
nonceSize := gcm.NonceSize() if len(ciphertext) < nonceSize { return nil, errors.New("ciphertext too short") } nonce := ciphertext[:nonceSize] data := ciphertext[nonceSize:] plaintext, err := gcm.Open(nil, nonce, data, nil) if err != nil { return nil, err }
Главное правило: для одного ключа нельзя повторять nonce. Повтор nonce в GCM может разрушить безопасность.
Секреты в конфигурации
Секреты должны приходить из переменных окружения или secret manager:
apiKey := os.Getenv("API_KEY") if apiKey == "" { return errors.New("API_KEY is required") }
Не храните .env с реальными ключами в репозитории. Вместо него добавьте .env.example без настоящих секретов.
Если секрет попал в Git, его нужно считать скомпрометированным и заменить.
Валидация ввода
Безопасность начинается с простой валидации:
- ограничьте длину строк;
- проверяйте формат email, URL, имени файла;
- проверяйте размер загружаемого файла;
- не доверяйте данным из HTTP-запроса;
- не передавайте пользовательский ввод в shell-команды без необходимости.
Валидация не должна быть только на frontend. Пользователь может отправить HTTP-запрос напрямую.
SQL injection
Не собирайте SQL через склеивание строк:
query := "SELECT * FROM users WHERE email = '" + email + "'"
Используйте placeholders:
row := db.QueryRowContext(ctx, "SELECT id, email FROM users WHERE email = $1", email, )
Так база получает SQL отдельно от данных и не воспринимает пользовательский ввод как часть команды.
Shell injection
Если нужно вызвать внешнюю команду, не склеивайте строку shell-команды с пользовательским вводом.
Плохо:
exec.Command("sh", "-c", "convert "+filename+" output.png")
Лучше:
exec.Command("convert", filename, "output.png")
Но даже здесь нужно проверять имя файла, расширение и доступные пути.
Логи
Логи должны помогать отладке, но не раскрывать приватные данные. Не логируйте:
- пароли;
- access и refresh tokens;
- API keys;
- private keys;
- номера карт;
- одноразовые reset-ссылки.
Если нужно идентифицировать token в логах, можно логировать короткий hash или последние несколько символов, но только если это действительно необходимо.
Чеклист
- Токены генерируются через
crypto/rand. - Пароли хранятся через
bcryptили другой password hashing алгоритм. - Секреты не попадают в Git.
- Шифрование использует AEAD-режим.
- Nonce не повторяется для одного ключа.
- Логи не содержат приватных данных.
- Пользовательский ввод валидируется до использования.
- SQL использует placeholders.
- Shell-команды не собираются одной строкой из пользовательского ввода.
- Frontend-валидация не считается защитой.