Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Zabezpiecz swoje aplikacje mssql-python, stosując się do najlepszych praktyk uwierzytelniania, parametryzowanych zapytań i ochrony danych.
Zacznij od uwierzytelniania bez hasła, kiedy tylko możesz. Lokalne pliki .env i hasła SQL traktuj jako tymczasowe rozwiązania pomocnicze na etapie programowania, a przed przeniesieniem kodu do środowiska współdzielonego przenieś wpisy tajne do zarządzanych tożsamości lub magazynu wpisów tajnych.
Zabezpieczenia uwierzytelniania
Użyj uwierzytelniania Microsoft Entra zamiast uwierzytelniania SQL
Uwierzytelnianie Microsoft Entra eliminuje przechowywane hasła i obsługuje zarządzane tożsamości. Wolę to od uwierzytelniania SQL we wszystkich środowiskach.
Dla obciążeń hostowanych w Azure używaj tożsamości zarządzanej z ActiveDirectoryMSI. Nie potrzebuje żadnych przechowywanych sekretów i łączy się bez konieczności przechodzenia przez łańcuch uwierzytelniający:
import mssql_python
def connect_with_managed_identity():
return mssql_python.connect(
"Server=<server>.database.windows.net;"
"Database=<database>;"
"Authentication=ActiveDirectoryMSI;"
"Encrypt=yes;"
)
Do lokalnego rozwoju użyj ActiveDirectoryDefault, który automatycznie odbiera Twoje dane uwierzytelniające Azure CLI lub inne poświadczenia deweloperskie. Unikaj tego w środowisku produkcyjnym, ponieważ DefaultAzureCredential przy pierwszym połączeniu po kolei wypróbowuje każdego dostawcę poświadczeń, co wprowadza dodatkowe opóźnienie, którego środowisko produkcyjne nie potrzebuje:
import mssql_python
def connect_with_default_credential():
return mssql_python.connect(
"Server=<server>.database.windows.net;"
"Database=<database>;"
"Authentication=ActiveDirectoryDefault;"
"Encrypt=yes;"
)
Unikaj: Uwierzytelnianie SQL przechowuje dane uwierzytelniające w kodzie/konfiguracji i jest podatne na wycieki.
conn = mssql_python.connect("Server=...;UID=user;PWD=password")
Nigdy nie koduj na stałe danych uwierzytelniających
Używaj zmiennych środowiskowych tylko do lokalnego rozwoju. W środowiskach współdzielonych preferuj uwierzytelnianie bezhasłowe. Gdy dziedziczny przepływ uwierzytelniania SQL jest nieunikniony, pobierz sekret w czasie działania z magazynu sekretów zamiast sprawdzać pełny ciąg parametry połączenia w kontroli źródeł.
Następująca metoda polega na twardym kodowaniu danych uwierzytelniających i nigdy nie powinna być używana:
conn_str = "Server=<server>;UID=<login>;PWD=<password>"
Na potrzeby lokalnego programowania odczytuj dane uwierzytelniające ze zmiennych środowiskowych:
import os
conn_str = (
f"Server={os.environ['DB_SERVER']};"
f"Database={os.environ['DB_NAME']};"
)
W środowiskach współdzielonych, które nadal wymagają sekretu, pobierz go w czasie działania z Azure Key Vault:
from azure.keyvault.secrets import SecretClient
from azure.identity import DefaultAzureCredential
def get_connection_string():
credential = DefaultAzureCredential()
secret_client = SecretClient(
vault_url="https://myvault.vault.azure.net/",
credential=credential
)
return secret_client.get_secret("db-connection-string").value
Zapobieganie wstrzykiwaniu SQL
Zawsze używaj zapytań parametryzowanych
Zapytania parametryzowane zapobiegają wstrzykiwaniu SQL, oddzielając wejście użytkownika od struktury zapytania. Zawsze parametryzuj dane wejściowe użytkownika.
Poniższe zapytanie sformatowane jako ciąg znaków jest podatne na atak typu SQL injection. Nigdy nie buduj zapytań w ten sposób:
user_input = "'; DROP TABLE Person.Person; --"
cursor.execute(f"SELECT * FROM Person.Person WHERE FirstName = '{user_input}'")
Następujące parametryzowane zapytanie jest bezpieczne, ponieważ sterownik wysyła wartość oddzielnie od tekstu zapytania:
user_input = "'; DROP TABLE Person.Person; --"
cursor.execute(
"SELECT * FROM Person.Person WHERE FirstName = %(name)s",
{"name": user_input}
)
Parametryzuj wszystkie komponenty zapytań
Nie da się bezpośrednio parametryzować nazw tabel i kolumn. Interpolacja ich z danych wejściowych od użytkownika sprawia, że twoja aplikacja jest podatna na zastrzyk SQL:
table = user_input
cursor.execute(f"SELECT * FROM {table}")
Zamiast tego zweryfikuj identyfikatory dynamiczne względem listy dozwolonych wartości i parametryzuj pozostałe wartości.
ALLOWED_TABLES = {"Person.Person", "Production.Product", "Sales.SalesOrderHeader"}
def query_table(cursor, table_name: str, conditions: dict):
"""Query with validated table name."""
if table_name not in ALLOWED_TABLES:
raise ValueError(f"Invalid table: {table_name}")
# Table name is safe, parameters are parameterized
where_clauses = [f"{k} = %({k})s" for k in conditions.keys()]
query = f"SELECT * FROM {table_name} WHERE {' AND '.join(where_clauses)}"
cursor.execute(query, conditions)
return cursor.fetchall()
Walidacja i oczyszczanie danych wejściowych
Budując dynamiczne SQL z identyfikatorami, waliduj każdą wartość według ścisłego wzorca przed jej użyciem:
import re
def validate_identifier(value: str) -> bool:
"""Validate SQL identifier (table/column name)."""
# Only allow alphanumeric and underscore
return bool(re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value))
def safe_order_by(cursor, table: str, order_column: str, direction: str):
"""Order by with validation."""
if not validate_identifier(order_column):
raise ValueError(f"Invalid column name: {order_column}")
if direction.upper() not in ("ASC", "DESC"):
raise ValueError(f"Invalid direction: {direction}")
cursor.execute(f"""
SELECT * FROM {table}
ORDER BY {order_column} {direction.upper()}
""")
Używaj procedur przechowywanych do złożonych operacji
Procedury przechowywane zmniejszają powierzchnię SQL wystawioną na kod aplikacyjny:
employee_id = 5
cursor.execute("""
EXECUTE dbo.uspGetEmployeeManagers @BusinessEntityID = %(id)s
""", {"id": employee_id})
Zabezpieczenia połączeń
Wymagane szyfrowanie
Zawsze szyfruj połączenia. Azure SQL domyślnie wymusza szyfrowanie. W przypadku lokalnego programu SQL Server ustaw Encrypt=yes jawnie:
conn = mssql_python.connect(
"Server=<server>.database.windows.net;"
"Database=<database>;"
"Encrypt=yes;"
"TrustServerCertificate=no"
)
Użyj trybu TDS 8.0 strict mode dla najwyższego bezpieczeństwa
TDS 8.0 zapewnia:
- TLS 1.3 od początku połączenia
- Wymagana weryfikacja certyfikatu
- Nie ma powrotu do starszych protokołów
conn = mssql_python.connect(
"Server=tcp:<server>.database.windows.net,1433;"
"Database=<database>;"
"Encrypt=strict"
)
Weryfikacja certyfikatów serwera
Zawsze weryfikuj certyfikat serwera w środowisku produkcyjnym, aby zapobiec atakom typu man-in-the-middle. Nie ustawaj TrustServerCertificate=yes, bo omija to walidację. Zamiast tego ustaw TrustServerCertificate=no, aby weryfikować względem certyfikatów CA, a HostNameInCertificate, aby weryfikować nazwę hosta:
conn = mssql_python.connect(
"Server=<server>;"
"Database=<database>;"
"Encrypt=yes;"
"TrustServerCertificate=no;"
"HostNameInCertificate=<server>.domain.com"
)
Ochrona danych
Chroń wrażliwe dane na poziomie serwera
Always Encrypted nie można obecnie skonfigurować za pomocą słów kluczowych parametrów połączenia mssql-python. Jeśli potrzebujesz Always Encrypted, użyj pyodbc ze sterownikiem ODBC dla SQL Server, który go obsługuje. Chociaż nie zapewniają takiego samego poziomu ochrony, możesz wykorzystać funkcje SQL Server, takie jak dynamiczne maskowanie danych i bezpieczeństwo na poziomie wiersza, aby chronić wrażliwe kolumny.
employee_id = 1
cursor.execute("""
SELECT NationalIDNumber, LoginID
FROM HumanResources.Employee
WHERE BusinessEntityID = %(id)s
""", {"id": employee_id})
row = cursor.fetchone()
Ochrona danych podczas przesyłania
- Zastosowanie
Encrypt=yesw łańcuchach połączeń. - Używaj VPN lub prywatnych punktów końcowych do połączeń lokalnych.
- Użyj Azure Private Link for Azure SQL.
Zasada najmniejszych uprawnień
Używaj minimalnych uprawnień do bazy danych
Konta aplikacji powinny mieć minimalne uprawnienia. Nie używaj sa ani db_owner do połączeń aplikacji.
-
Raportowanie tylko do odczytu:
GRANT SELECT ON SCHEMA::dbo TO ReportingApp; -
Dostęp do konkretnych tabel:
GRANT SELECT, INSERT, UPDATE ON Orders TO OrderProcessor; -
Tylko procedura przechowywana:
GRANT EXECUTE ON ProcessOrder TO OrderProcessor;
Używaj różnych kont do różnych operacji
Przypisz każdemu poziomowi uprawnień własną tożsamość, aby ścieżka odczytu nie mogła wykonywać operacji zapisu. Ten przykład wykorzystuje dwie zarządzane tożsamości przypisane przez użytkownika: jedną z dostępem tylko do odczytu, a drugą z uprawnieniami do zapisu, wybieraną według identyfikatora klienta:
readonly_client_id = os.environ["READONLY_IDENTITY_CLIENT_ID"]
readwrite_client_id = os.environ["READWRITE_IDENTITY_CLIENT_ID"]
def get_readonly_connection():
"""Connection for read-only operations."""
return mssql_python.connect(
f"Server={server};Database={db};"
f"Authentication=ActiveDirectoryMSI;UID={readonly_client_id};"
f"Encrypt=yes;ApplicationIntent=ReadOnly;"
)
def get_readwrite_connection():
"""Connection for write operations."""
return mssql_python.connect(
f"Server={server};Database={db};"
f"Authentication=ActiveDirectoryMSI;UID={readwrite_client_id};"
f"Encrypt=yes;"
)
Zachowaj sekrety ról aplikacji z dala od kontroli wersji
Hasła do ról aplikacji nadal są tajemnicą. Przechowuj je w sejfie haseł lub w zmiennej środowiskowej zasilanej sekretami i rotuj je z taką samą ostrożnością jak wszelkie inne dane uwierzytelniające.
def execute_with_role(cursor, role: str, query: str, params: dict):
"""Execute query with specific application role."""
# Activate application role
cursor.execute(
"EXECUTE sp_setapprole @rolename = %(role)s, @password = %(pwd)s",
{"role": role, "pwd": os.environ[f"ROLE_{role.upper()}_PWD"]}
)
try:
cursor.execute(query, params)
return cursor.fetchall()
finally:
# Reset to original context
cursor.execute("EXECUTE sp_unsetapprole")
Inspekcja i rejestrowanie
Zdarzenia bezpieczeństwa w logach
Nie rejestruj wrażliwych danych, ale rejestruj zdarzenia istotne dla bezpieczeństwa, takie jak nieudane połączenia, błędy uprawnień i podejrzane zapytania.
import logging
logger = logging.getLogger("db_security")
def secure_connect(connection_string: str):
"""Connect with security logging."""
logger.info("Attempting database connection")
try:
conn = mssql_python.connect(connection_string)
logger.info("Database connection established")
return conn
except mssql_python.OperationalError as e:
logger.warning(f"Database connection failed: {type(e).__name__}")
raise
Operacje wrażliwe na audyt
Konfiguruj audyt baz danych dla wrażliwych tabel i operacji. Możesz także wdrożyć audyt na poziomie aplikacji dla kluczowych działań.
def audit_data_access(cursor, user_id: str, action: str, resource: str):
"""Log data access for audit trail."""
cursor.execute("""
INSERT INTO AuditLog (UserID, Action, Resource, Timestamp, IPAddress)
VALUES (%(user)s, %(action)s, %(resource)s, GETUTCDATE(), %(ip)s)
""", {
"user": user_id,
"action": action,
"resource": resource,
"ip": get_client_ip()
})
Nigdy nie rejestruj wrażliwych danych
Nie włączaj wartości parametrów do komunikatów logowych. Rejestruj operację, nie dane.
Unikaj – przecieka wartość do logu:
nid = "295847284"
logger.debug(f"Query: SELECT * FROM HumanResources.Employee WHERE NationalIDNumber = '{nid}'")
Polecam – loguje intencję bez ujawniania wartości:
logger.debug("Executing employee lookup query")
nid = "295847284"
cursor.execute("SELECT * FROM HumanResources.Employee WHERE NationalIDNumber = %(nid)s", {"nid": nid})
Obsługa błędów
Nie ujawniaj szczegółów wewnętrznych
Rejestruj szczegółowe błędy wewnętrznie, ale zwracaj użytkownikom ogólne komunikaty, aby uniknąć wycieku struktury bazy danych:
def safe_query(cursor, query: str, params: dict):
"""Execute query with safe error handling."""
try:
cursor.execute(query, params)
return cursor.fetchall()
except mssql_python.ProgrammingError as e:
# Log full error internally
logging.error(f"Query error: {e}")
# Return generic error to user
raise UserFacingError("An error occurred processing your request")
except mssql_python.IntegrityError:
raise UserFacingError("Invalid data provided")
Oczyszcz komunikaty błędów
Mapuj wyjątki bazy danych na komunikaty przyjazne dla użytkownika, które nie ujawniają szczegółów implementacji:
class UserFacingError(Exception):
"""Exception safe to show to users."""
pass
def handle_database_error(error: Exception) -> str:
"""Convert database errors to safe user messages."""
if isinstance(error, mssql_python.IntegrityError):
if "UNIQUE" in str(error):
return "A record with this value already exists"
if "FOREIGN KEY" in str(error):
return "Referenced item not found"
return "An error occurred. Please try again later."
Lista kontrolna zabezpieczeń
Lista kontrolna bezpieczeństwa połączenia
- [ ] Używaj uwierzytelniania Microsoft Entra, gdy to możliwe.
- [ ] Włącz szyfrowanie (
Encrypt=yes). - [ ] Zweryfikować certyfikaty serwera.
- [ ] Przechowywanie danych w bezpiecznym sejfie.
- [ ] Przechowywać
.envpliki tylko lokalnie i wstrzykywać sekrety przez docelową platformę w środowiskach współdzielonych. - [ ] Użyj trybu ścisłego TDS 8.0 dla Azure SQL.
Bezpieczeństwo zapytań
- [ ] Zawsze używaj parametryzowanych zapytań.
- [ ] Waliduj dynamiczne identyfikatory.
- [ ] Używaj procedur przechowywanych do złożonej logiki.
- [ ] Ogranicz rozmiar wyników zapytań.
Bezpieczeństwo danych
- [ ] Stosuj ochronę danych na poziomie serwera (maskowanie, bezpieczeństwo na poziomie wiersza).
- [ ] Stosuj zabezpieczenia na poziomie rzędów tam, gdzie to stosowne.
- [ ] Maskuj wrażliwe dane w logach.
Kontrola dostępu
- [ ] Stosuj zasadę najmniejszych przywilejów.
- [ ] Oddzielne konta do odczytu/zapisu.
- [ ] Regularnie audytuj uprawnienia.
- [ ] Zaimplementuj limity czasu połączeń.
Nadzorowanie
- [ ] Rejestruj zdarzenia bezpieczeństwa.
- [ ] Monitoruj pod kątem anomalii.
- [ ] Skonfiguruj alerty o awariach.
- [ ] Przeprowadzaj regularne kontrole bezpieczeństwa.