Najlepsze praktyki bezpieczeństwa z użyciem go-mssqldb

Ten artykuł omawia praktyki bezpieczeństwa dla aplikacji Go, które łączą się z SQL Server za pomocą sterownikago-mssqldb. Dotyczy on zapobiegania wtryskowi SQL, zarządzania poświadczeniami, konfiguracji szyfrowania oraz zasady najmniejszych uprawnień.

Zapobieganie wstrzyknięciu kodu SQL

SQL injection jest najczęstszą luką w bezpieczeństwie bazy danych. Sterownik go-mssqldb dostarcza parametryzowane zapytania, które oddzielają kod SQL od wartości danych. Zawsze używaj parametrów wejściowych dostarczanych przez użytkownika.

Używanie zapytań sparametryzowanych

Parametry są wysyłane oddzielnie od tekstu SQL, więc dane wejściowe użytkownika nigdy nie mogą być interpretowane jako kod 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))

Nigdy nie łącz danych wejściowych użytkownika do SQL

Konkatenacja łańcuchów pozwala atakującym wstrzykiwać dowolny kod 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;--" ...

Nazwy tabel lub kolumn dynamicznych

Nie można używać parametryzowanych zapytań do nazw tabel, kolumn ani innych identyfikatorów. Jeśli Twoja aplikacja wymaga identyfikatorów dynamicznych, zweryfikowaj je na podstawie listy dozwolonych:

// 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))
}

Uwaga

Nigdy nie umieszczaj danych wejściowych dostarczonych przez użytkownika bezpośrednio w identyfikatorach SQL, nawet przy użyciu nawiasów kwadratowych jako mechanizmu ucieczki. Zawsze waliduj według listy dozwolonych wartości.

Procedury przechowywane i wstrzykiwanie SQL

Procedury przechowywane nie zapobiegają automatycznie wstrzyknięciu SQL. Jeśli procedura wewnętrznie używa EXECUTE lub sp_executesql z połączonymi ciągami, nadal może być podatna na ataki. Używaj parametryzowanych wywołań:

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

Zarządzanie sekretami parametry połączenia

Ciągi połączeń mogą zawierać hasła, tajemnice klienta oraz ścieżki certyfikatów. Nigdy nie przechowuj ich w kodzie źródłowym.

Używanie zmiennych środowiskowych

Odczytaj pełne parametry połączenia ze zmiennej środowiskowej:

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

Buduj ciągi połączeń z pojedynczych sekretów

Złóż parametry połączenia z oddzielnych zmiennych środowiskowych dla każdego komponentu:

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())

Całkowicie usuń hasła

Najbezpieczniejsze parametry połączenia w ogóle nie zawierają hasła. Użyj uwierzytelniania Microsoft Entra ID z tożsamością zarządzaną:

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")

Opcje tajnego przechowywania

Środowisko Zalecane przechowywanie Notatki
Rozwój lokalny Zmienne środowiskowe lub plik sekretów użytkownika Nie zatwierdzaj plików .env w kontroli wersji.
Azure App Service / Container Apps Ustawienia aplikacji lub odniesienia do Key Vault Odwołania do usługi Key Vault wstrzykują wpisy tajne jako zmienne środowiskowe w czasie wykonywania.
Kubernetes Obiekty Secret platformy Kubernetes lub Azure Key Vault ze sterownikiem CSI Sekrety są montowane jako pliki lub zmienne środowiskowe.
Potoki CI/CD Sekrety potoku / Sekrety GitHub Actions Use ActiveDirectoryServicePrincipal or ActiveDirectoryAzurePipelines for Azure SQL.

Ważna

Dodaj .env, *.pfx i *.pem do pliku .gitignore. Przypadkowe dodanie tajnych danych do repozytorium jest jedną z najczęstszych przyczyn wycieku danych uwierzytelniających.

Konfigurowanie szyfrowania

Używaj szyfrowania dla wszystkich zdalnych połączeń

Ustaw encrypt=true dla wszystkich połączeń zdalnych. Szyfrowanie wymagane tylko przez serwer jest podatne na ataki przeciwnika pośrodku. Klient musi egzekwować szyfrowanie. Azure SQL domyślnie umożliwia szyfrowanie:

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

Użyj TDS 8.0 do najsilniejszego szyfrowania

TDS 8.0 (tryb ścisły) wykonuje uścisk TLS przed wymianą jakichkolwiek danych protokołu TDS, zapobiegając atakom na obniżenie poziomu protokołu:

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

TDS 8.0 wymaga SQL Server 2022 lub Azure SQL.

Nie używaj TrustServerCertificate w produkcji

Ustawienie TrustServerCertificate=true wyłącza weryfikację certyfikatów, co umożliwia ataki typu man-in-the-middle:

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

Do produkcji należy skonfigurować odpowiednią walidację certyfikatów. Zobacz Szyfrowanie i certyfikaty.

Ustaw minimalną wersję TLS

Wymusz TLS 1.2 lub nowszy, aby zapobiec atakom na obniżenie poziomu protokołu.

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

Stosowanie zasady najniższych uprawnień

Używaj użytkowników ograniczonych do bazy danych

Twórz zamkniętych użytkowników baz danych ograniczonych do jednej bazy zamiast logowania na poziomie serwera, które mają dostęp do wielu baz danych.

-- 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];

Oddzielne połączenia odczytu i zapisu

Jeśli Twoja aplikacja ma wyraźne ścieżki odczytu i zapisu, stwórz osobne użytkowników bazy danych z odpowiednimi uprawnieniami.

// 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")

Unikaj stosowania SA lub DBO w aplikacjach

Konto sa i użytkownik dbo mają nieograniczony dostęp. Jeśli aplikacja połączona jako sa ma podatność umożliwiającą wstrzyknięcie kodu SQL, atakujący zyskuje pełną kontrolę nad serwerem bazy danych.

Chroń klucze Always Encrypted

Gdy używasz Always Encrypted, klucz główny kolumny (CMK) chroni klucze szyfrujące kolumny (CEK). Odpowiednio zabezpiecz CMK:

Dostawca kluczy Najlepsze rozwiązanie
Certyfikat lokalny (PFX) Przechowywaj plik PFX poza katalogiem aplikacji. Ustaw ograniczające uprawnienia do plików. Użyj zmiennej środowiskowej jako hasła.
Magazyn certyfikatów systemu Windows Użyj CurrentUser\My w przypadku kont serwisowych. Ustaw uprawnienia certyfikatu, używając certutil.
Azure Key Vault Używaj zarządzanej tożsamości do dostępu. Ustaw polityki dostępu do sejfu kluczy z najmniejszymi uprawnieniami. Włącz usuwanie miękkie i ochronę przed trwałym usunięciem.

Aby uzyskać konfigurację dostawcy klucza, zobacz Zawsze szyfrowane.

Loguj się bezpiecznie

Nie loguj ciągów połączeń ani poświadczeń

Ciągi połączeń mogą zawierać hasła, które trafiają do plików logów:

// 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)

Jeśli musisz zapisać szczegóły połączeń do diagnostyki, najpierw zredaguj parametry połączenia:

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()
}

Rejestruj ocenzurowaną wartość tylko wtedy, gdy jest potrzebna do krótkotrwałej diagnostyki. Wolę rejestrować nazwę serwera, nazwę bazy danych oraz tryb uwierzytelniania osobno.

Nie loguj wrażliwych parametrów zapytań

Unikaj logowania wartości parametrów zawierających dane osobowe lub wrażliwe:

// 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")

Ostrożnie używaj flag dziennika w środowisku produkcyjnym

Parametr log sterownika może zwracać instrukcje SQL i wartości parametrów:

Flag Ryzyko Bezpieczny dla produkcji?
1 (błędy) Low Yes
2 (wiadomości) Low Yes
4 (wioski) Średni, ujawnia dane No
8 (SQL) Medium, eksponuje zapytania Warunkowy
16 (params) Wysoki, eksponuje wartości parametrów No
32 (transakcje) Low Yes
64 (debugowanie) Wysoki poziom, ujawnia szczegóły protokołu No

W produkcji używaj log=1 (tylko błędy) lub log=3 (błędy + komunikaty).

Lista kontrolna zabezpieczeń

Area Zalecenie
Wstrzyknięcie kodu SQL Do wszystkich danych wejściowych używaj parametryzowanych zapytań. Zweryfikuj identyfikatory dynamiczne względem listy dozwolonych.
Credentials W miarę możliwości używaj tożsamości zarządzanej Microsoft Entra. Nigdy nie koduj na stałe haseł w kodzie źródłowym.
Encryption Ustaw encrypt=true lub encrypt=strict dla wszystkich połączeń w środowisku produkcyjnym. Ustaw wartość tlsmin=1.2.
Certificates Nie używaj TrustServerCertificate=true w środowisku produkcyjnym. Weryfikuj certyfikaty serwera.
Najmniej uprzywilejowane Utworzenie użytkowników bazy danych specyficznych dla aplikacji z jedynie wymaganymi uprawnieniami.
Secrets Przechowywać ciągi połączeń w zmiennych środowiskowych, Key Vault lub w tajnych magazynach platformy.
Logging Nie loguj ciągów połączeń, haseł ani wrażliwych parametrów.
Kontrola źródła Dodaj .env, *.pfx, oraz *.pem do .gitignore.