Лучшие практики безопасности с помощью go-mssqldb

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

Предотвращение SQL-инъекций

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

Использование параметризованных запросов

Параметры отправляются отдельно от SQL-текста, поэтому пользовательский ввод никогда не может быть интерпретирован как SQL-код:

// CORRECT: Parameters are sent separately from the SQL text.
rows, err := db.QueryContext(ctx,
    "SELECT * FROM Sales.vSalesPerson WHERE FirstName = @name AND CountryRegionName = @loc",
    sql.Named("name", userName),
    sql.Named("loc", userLocation))

Никогда не конкатенируйте пользовательский ввод в SQL

Конкатенация строк позволяет злоумышленникам внедрять произвольный SQL:

// WRONG: SQL injection vulnerability.
query := fmt.Sprintf("SELECT * FROM Sales.vSalesPerson WHERE FirstName = '%s'", userName)
rows, err := db.QueryContext(ctx, query) // If userName is "'; DROP TABLE HumanResources.Department;--" ...

Динамические названия таблиц или столбцов

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

// Validate against known-safe values.
var validColumns = map[string]bool{
    "FirstName": true, "CountryRegionName": true, "TerritoryName": true,
}

func queryByColumn(ctx context.Context, db *sql.DB, column, value string) (*sql.Rows, error) {
    if !validColumns[column] {
        return nil, fmt.Errorf("invalid column name: %q", column)
    }
    // Safe to use column directly because it was validated against the allowlist.
    query := fmt.Sprintf("SELECT * FROM Sales.vSalesPerson WHERE [%s] = @p1", column)
    return db.QueryContext(ctx, query, sql.Named("p1", value))
}

Предостережение

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

Хранящиеся процедуры и SQL-инъекции

Хранимые процедуры не предотвращают SQL-инъекции автоматически. Если процедура использует EXECUTE или sp_executesql с конкатенированными строками внутри неё, она всё равно может быть уязвимой. Используйте параметризованные вызовы:

// Parameters are passed safely to the procedure.
_, err := db.ExecContext(ctx, "dbo.SearchEmployees",
    sql.Named("searchTerm", userInput))

Управление секретами строки подключения

Строки соединения могут содержать пароли, клиентские секреты и пути к сертификатам. Никогда не храните их в исходном коде.

Использование переменных среды

Считайте полную строку подключения из переменной среды:

connString := os.Getenv("MSSQL_CONNECTION_STRING")
if connString == "" {
    log.Fatal("MSSQL_CONNECTION_STRING environment variable is required")
}
db, err := sql.Open("sqlserver", connString)

Постройте строки соединения из отдельных секретов

Соберите строку подключения из отдельных переменных среды для каждого компонента:

import (
    "net/url"
    "os"
)

query := url.Values{}
query.Add("database", os.Getenv("DB_NAME"))
query.Add("encrypt", "true")

password := os.Getenv("DB_PASSWORD")

u := &url.URL{
    Scheme:   "sqlserver",
    User:     url.UserPassword(os.Getenv("DB_USER"), password),
    Host:     os.Getenv("DB_HOST"),
    RawQuery: query.Encode(),
}
db, err := sql.Open("sqlserver", u.String())

Полностью убрать пароли

Самая безопасная строка подключения вообще не содержит пароля. Используйте аутентификацию Microsoft Entra ID с управляемой идентификацией:

import _ "github.com/microsoft/go-mssqldb/azuread"

// No password, no secret. The managed identity handles authentication.
db, err := sql.Open("azuresql",
    "sqlserver://<server>.database.windows.net?database=AdventureWorks2025&fedauth=ActiveDirectoryDefault&encrypt=true&TrustServerCertificate=false")

Варианты секретного хранения

Окружающая среда Рекомендуемое хранилище Примечания
Местное развитие Переменные среды или файл пользовательских секретов Не добавляйте файлы .env в систему контроля версий.
Служба приложений Azure / Container Apps Настройки приложения или ссылки на хранилище ключей Key Vault Key Vault ссылается на ввод секретов в качестве переменных среды во время выполнения.
Kubernetes секреты Kubernetes или Azure Key Vault с драйвером CSI Секреты монтируются в виде файлов или переменных среды.
Конвейеры CI/CD Секреты конвейера / секреты GitHub Actions Используйте ActiveDirectoryServicePrincipal или ActiveDirectoryAzurePipelines для Azure SQL.

Important

Добавьте .env, *.pfx, и *.pem в ваш .gitignore файл. Случайное сохранение секретов в репозиторий — один из самых распространённых источников утечек учетных данных.

Настройка шифрования

Используйте шифрование для всех удалённых соединений

Настройте encrypt=true на все удалённые подключения. Шифрование, необходимое только серверу, уязвимо для атак противника посередине. Клиент должен обеспечивать шифрование. Azure SQL по умолчанию поддерживает шифрование:

sqlserver://<user>:<password>@<server>:1433?database=AdventureWorks2025&encrypt=true

Используйте TDS 8.0 для надёжного шифрования

TDS 8.0 (строгий режим) выполняет TLS-рукопожатие до обмена данными протокола TDS, предотвращая атаки на понижение уровня протокола:

sqlserver://<user>:<password>@<server>:1433?database=AdventureWorks2025&encrypt=strict

TDS 8.0 требует SQL Server 2022 или Azure SQL.

Не используйте TrustServerCertificate в продакшене

Установка TrustServerCertificate=true отключает проверку сертификатов, что делает возможными атаки типа «злоумышленник посередине»:

// DEVELOPMENT ONLY. Never deploy to production.
connString := "sqlserver://<user>:<password>@<server>?database=AdventureWorks2025&TrustServerCertificate=true"

Для продакшена настройте правильную валидацию сертификатов. См. Шифрование и сертификаты.

Установите минимальную TLS-версию

Применяйте TLS 1.2 или более позднее для предотвращения атак на понижение уровня протокола.

sqlserver://<user>:<password>@<server>:1433?database=AdventureWorks2025&encrypt=true&tlsmin=1.2

Применяйте принцип минимально необходимых привилегий

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

Создавайте изолированных пользователей базы данных, действующих в пределах одной базы данных, вместо учётных записей уровня сервера, обеспечивающих доступ к нескольким базам данных.

-- Create a contained database user (no server-level login needed).
CREATE USER [app_user] WITH PASSWORD = '<password>';

-- Grant only the permissions the application needs.
ALTER ROLE db_datareader ADD MEMBER [app_user];
ALTER ROLE db_datawriter ADD MEMBER [app_user];
GRANT EXECUTE ON SCHEMA::dbo TO [app_user];

Отдельные соединения чтения и записи

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

// Read-only queries use a restricted user.
readDB, _ := sql.Open("sqlserver",
    "sqlserver://app_reader:password@<server>?database=AdventureWorks2025&ApplicationIntent=ReadOnly&encrypt=true")

// Write operations use a user with write permissions.
writeDB, _ := sql.Open("sqlserver",
    "sqlserver://app_writer:password@<server>?database=AdventureWorks2025&encrypt=true")

Избегайте использования SA или dbo в приложениях

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

Защита всегда зашифрованных ключей

Когда вы используете Always Encrypted, главный ключ столбца (CMK) защищает ключевые ключи шифрования столбцов (CEK). Правильно защитите CMK:

Поставщик ключей Лучшие практики
Локальный сертификат (PFX) Храните PFX-файл вне каталога приложений. Установите ограничительные права на файлы. Используйте переменную среды для пароля.
Хранилище сертификатов Windows Используйте CurrentUser\My для сервисных аккаунтов. Установите права доступа для сертификатов с помощью certutil.
Azure Key Vault Используйте управляемую идентичность для доступа. Установите политики доступа к хранилищу ключей с наименьшими привилегиями. Включите мягкое удаление и защиту от окончательного удаления.

Для конфигурации поставщика ключей см. раздел «Всегда зашифровано».

Бережно записывать лог

Не записывайте строки соединений или учетные данные

Строки соединения могут содержать пароли, которые попадают в лог-файлы:

// WRONG: Logs the password.
log.Printf("Connecting to %s", connString)

// CORRECT: Log only the server and database.
log.Printf("Connecting to server=%s database=%s", serverName, dbName)

Если нужно записывать детали соединения для диагностики, сначала отредактируйте строка подключения:

func redactSQLServerURL(raw string) string {
    u, err := url.Parse(raw)
    if err != nil {
        return "<invalid connection string>"
    }

    if u.User != nil {
        username := u.User.Username()
        if username != "" {
            u.User = url.UserPassword(username, "REDACTED")
        }
    }

    q := u.Query()
    for _, key := range []string{"password", "clientassertion", "systemtoken"} {
        if q.Has(key) {
            q.Set(key, "REDACTED")
        }
    }
    u.RawQuery = q.Encode()
    return u.String()
}

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

Не фиксируйте чувствительные параметры запроса

Избегайте логирования параметров, содержащих личные или конфиденциальные данные:

// WRONG: Logs sensitive parameter values.
log.Printf("Query: SELECT * FROM HumanResources.Employee WHERE NationalIDNumber = %s", nationalIDNumber)

// CORRECT: Log the query structure without parameter values.
log.Printf("Querying HumanResources.Employee by NationalIDNumber")

Осторожно используйте флаги журналирования в рабочей среде

Параметр драйвера log может выводить SQL-операторы и значения параметров:

Flag Риск Безопасно в производстве?
1 (ошибки) Low Да
2 (сообщения) Low Да
4 (строки) Носитель, предоставляющий доступ к данным Нет
8 (SQL) Medium, предоставляет доступ к запросам Conditional
16 (параметры) Высокий, показывает значения параметров Нет
32 (транзакции) Low Да
64 (отладка) Высокое, раскрывает детали протокола Нет

Для продакшна используйте log=1 (только ошибки) или log=3 (ошибки + сообщения).

Контрольный список безопасности

Area Recommendation
SQL-инъекция Используйте параметризованные запросы для всех пользовательских вводных данных. Проверяйте динамические идентификаторы по списку разрешённых значений.
Credentials Используйте управляемую идентичность Microsoft Entra, когда это возможно. Никогда не закодуйте пароли в исходном коде.
Encryption Установите encrypt=true или encrypt=strict для всех рабочих подключений. Задайте tlsmin=1.2.
Сертификаты Не используйте TrustServerCertificate=true в рабочей среде. Проверьте серверные сертификаты.
Наименьшие привилегии Создайте пользователей базы данных, специфичных для приложений, с необходимыми правами.
Secrets Храните строки соединений в переменных среды, Key Vault или секретных хранилищах платформы.
Logging Не записывайте строки соединений, пароли или чувствительные значения параметров.
Управление исходным кодом Добавьте .env, *.pfx, и *.pem в .gitignore.