Содержание

Безопасность и криптография в 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 или последние несколько символов, но только если это действительно необходимо.

Чеклист

  1. Токены генерируются через crypto/rand.
  2. Пароли хранятся через bcrypt или другой password hashing алгоритм.
  3. Секреты не попадают в Git.
  4. Шифрование использует AEAD-режим.
  5. Nonce не повторяется для одного ключа.
  6. Логи не содержат приватных данных.
  7. Пользовательский ввод валидируется до использования.
  8. SQL использует placeholders.
  9. Shell-команды не собираются одной строкой из пользовательского ввода.
  10. Frontend-валидация не считается защитой.

Следующий шаг после статьи

Закрепите тему в реальном проекте и продолжайте обучение на курсах Praxis.

Продолжить изучение

Выбери следующую статью по маршруту или углубись в смежную тему.

Похожие статьи