Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этой статье рассматриваются практики безопасности приложений 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. |