Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Sécurisez vos applications mssql-python en suivant ces meilleures pratiques pour l’authentification, les requêtes paramétrées et la protection des données.
Commencez par une authentification sans mot de passe dès que vous le pouvez. Traitez les fichiers locaux .env et les mots de passe SQL comme des aides temporaires au développement, et transférez les secrets dans des identités gérées ou un stockage secret avant que le code n’atteigne un environnement partagé.
Sécurité de l’authentification
Utilisez l’authentification Microsoft Entra plutôt que l’authentification SQL
L’authentification Microsoft Entra élimine les mots de passe stockés et prend en charge les identités gérées. Je la préfère à l’authentification SQL dans tous les environnements.
Pour les charges de travail hébergées dans Azure, utilisez une identité gérée avec ActiveDirectoryMSI. Il n’a pas besoin de secrets stockés et se connecte sans passer par une chaîne d’accréditation :
import mssql_python
def connect_with_managed_identity():
return mssql_python.connect(
"Server=<server>.database.windows.net;"
"Database=<database>;"
"Authentication=ActiveDirectoryMSI;"
"Encrypt=yes;"
)
Pour le développement local, utilisez ActiveDirectoryDefault, qui récupère automatiquement votre Azure CLI ou d’autres identifiants de développeur. Évitez-le en production, car DefaultAzureCredential essaie chaque fournisseur d’informations d’identification dans l’ordre lors de la première connexion, ce qui ajoute une latence dont les charges de travail de production n’ont pas besoin :
import mssql_python
def connect_with_default_credential():
return mssql_python.connect(
"Server=<server>.database.windows.net;"
"Database=<database>;"
"Authentication=ActiveDirectoryDefault;"
"Encrypt=yes;"
)
À éviter : L’authentification SQL stocke les identifiants dans le code/la configuration et est vulnérable aux fuites.
conn = mssql_python.connect("Server=...;UID=user;PWD=password")
Ne codez jamais les identifiants en dur
Utilisez uniquement les variables d’environnement pour le développement local. Dans les environnements partagés, il faut privilégier l’authentification sans mot de passe. Quand un flux d’authentification SQL hérité est inévitable, récupérez le secret à l’exécution depuis un stockage secret au lieu de vérifier une chaîne de connexion complète dans le contrôle de version.
L’approche suivante code durement les identifiants et ne doit jamais être utilisée :
conn_str = "Server=<server>;UID=<login>;PWD=<password>"
Pour le développement local, lisez les identifiants à partir des variables de l’environnement :
import os
conn_str = (
f"Server={os.environ['DB_SERVER']};"
f"Database={os.environ['DB_NAME']};"
)
Pour les environnements partagés qui nécessitent encore un secret, récupérez-le à l’exécution depuis 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
Prévention de l’injection de code SQL
Utilisez toujours des requêtes paramétrées
Les requêtes paramétrées empêchent l’injection SQL en séparant l’entrée utilisateur de la structure de requête. Parametriser toujours les entrées de l’utilisateur.
La requête suivante formatée en chaîne de caractères est vulnérable à l’injection SQL. Ne construisez jamais de requêtes de cette façon :
user_input = "'; DROP TABLE Person.Person; --"
cursor.execute(f"SELECT * FROM Person.Person WHERE FirstName = '{user_input}'")
La requête paramétrée suivante est sûre, car le pilote envoie la valeur séparément du texte de la requête :
user_input = "'; DROP TABLE Person.Person; --"
cursor.execute(
"SELECT * FROM Person.Person WHERE FirstName = %(name)s",
{"name": user_input}
)
Paramétrez tous les composants de requête
Vous ne pouvez pas paramétrer directement les noms des tables et des colonnes. Les interpoler à partir des saisies de l’utilisateur rend votre application vulnérable à l’injection SQL :
table = user_input
cursor.execute(f"SELECT * FROM {table}")
À la place, validez les identifiants dynamiques par rapport à une liste de permis de valeurs permises, et paramétrez les valeurs restantes.
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()
Valider et désinfecter les entrées
Lorsque vous construisez du SQL dynamique avec des identifiants, validez chaque valeur par rapport à un schéma strict avant de l’utiliser :
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()}
""")
Utiliser des procédures stockées pour des opérations complexes
Les procédures stockées réduisent la surface SQL exposée au code applicatif :
employee_id = 5
cursor.execute("""
EXECUTE dbo.uspGetEmployeeManagers @BusinessEntityID = %(id)s
""", {"id": employee_id})
Sécurité de la connexion
Nécessite un chiffrement
Chiffrez toujours les connexions. Azure SQL applique le chiffrement par défaut. Pour le SQL Server sur site, définissez Encrypt=yes explicitement :
conn = mssql_python.connect(
"Server=<server>.database.windows.net;"
"Database=<database>;"
"Encrypt=yes;"
"TrustServerCertificate=no"
)
Utilisez le mode strict TDS 8.0 pour la sécurité la plus élevée
TDS 8.0 propose :
- TLS 1.3 depuis le début de la connexion
- Validation de certificats requise
- Pas de recours aux anciens protocoles
conn = mssql_python.connect(
"Server=tcp:<server>.database.windows.net,1433;"
"Database=<database>;"
"Encrypt=strict"
)
Valider les certificats serveur
Validez toujours le certificat serveur en production pour prévenir les attaques adversaires intermédiaires. Ne définissez pas TrustServerCertificate=yes, car cela permet de contourner la validation. À la place, configurez TrustServerCertificate=no pour valider avec les certificats de l’AC, et HostNameInCertificate pour vérifier le nom d’hôte :
conn = mssql_python.connect(
"Server=<server>;"
"Database=<database>;"
"Encrypt=yes;"
"TrustServerCertificate=no;"
"HostNameInCertificate=<server>.domain.com"
)
Protection de données
Protéger les données sensibles au niveau serveur
Always Encrypted n'est pas actuellement configurable via les mots-clés de chaîne de connexion mssql-python. Si vous avez besoin d’Ever Encrypted, utilisez pyodbc avec le pilote ODBC pour SQL Server, qui le supporte. Bien qu'ils n'offrent pas le même niveau de protection, vous pouvez utiliser des fonctionnalités de SQL Server comme le masquage dynamique des données et la sécurité au niveau des lignes pour protéger les colonnes sensibles.
employee_id = 1
cursor.execute("""
SELECT NationalIDNumber, LoginID
FROM HumanResources.Employee
WHERE BusinessEntityID = %(id)s
""", {"id": employee_id})
row = cursor.fetchone()
Protection des données en transit
- Utilisez
Encrypt=yesdans les chaînes de connexion. - Utilisez un VPN ou des points de terminaison privés pour les connexions sur site.
- Utilisez Azure Private Link pour Azure SQL.
Principe du privilège minimum
Utilisez des permissions minimales de base de données
Les comptes applications devraient avoir des permissions minimales. N’utilisez pas sa ou db_owner pour les connexions d’application.
-
Reportage en lecture seule :
GRANT SELECT ON SCHEMA::dbo TO ReportingApp; -
Accès spécifique à la table :
GRANT SELECT, INSERT, UPDATE ON Orders TO OrderProcessor; -
Procédure stockée seulement :
GRANT EXECUTE ON ProcessOrder TO OrderProcessor;
Utilisez différents comptes pour différentes opérations
Sauvegardez chaque niveau de privilège avec sa propre identité afin qu’un chemin de lecture ne puisse pas effectuer d’écritures. Cet exemple utilise deux identités managées attribuées par l’utilisateur, l’une accordée à l’accès en lecture seule et l’autre à l’écriture, sélectionnées par identifiant client :
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;"
)
Gardez les secrets des rôles applicatifs hors du contrôle de version
Les mots de passe pour les postes d’application restent des secrets. Conservez-les dans un coffre-fort de secrets ou dans une variable d’environnement alimentée par un secret, et effectuez leur rotation avec le même soin que pour toute autre information d’authentification.
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")
Audit et journalisation
Enregistrer les événements de sécurité
Ne consignez pas les données sensibles, mais enregistrez les événements liés à la sécurité comme les connexions défaillantes, les erreurs d’autorisation et les requêtes suspectes.
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
Opérations sensibles à l’audit
Configurez l’audit de base de données pour les tables et opérations sensibles. Vous pouvez également mettre en place un audit au niveau de l’application pour les actions critiques.
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()
})
Ne jamais enregistrer les données sensibles
N’incluez pas les valeurs des paramètres dans les messages du journal. Enregistrez l’opération, pas les données.
Éviter - fait fuir la valeur dans le journal :
nid = "295847284"
logger.debug(f"Query: SELECT * FROM HumanResources.Employee WHERE NationalIDNumber = '{nid}'")
Recommandé - enregistre l’intention sans exposer les valeurs :
logger.debug("Executing employee lookup query")
nid = "295847284"
cursor.execute("SELECT * FROM HumanResources.Employee WHERE NationalIDNumber = %(nid)s", {"nid": nid})
Gestion des erreurs
Ne révèle pas les détails internes
Consignez les erreurs détaillées en interne mais renvoyez des messages génériques aux utilisateurs pour éviter la fuite de la structure de la base de données :
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")
Désinfecter les messages d’erreur
Associez les exceptions de base de données à des messages clairs pour l’utilisateur qui ne révèlent pas les détails d’implémentation :
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."
Liste de contrôle de sécurité
Liste de contrôle de la sécurité des connexions
- [ ] Utilisez l’authentification Microsoft Entra lorsque c’est possible.
- [ ] Activer le chiffrement (
Encrypt=yes). - [ ] Validez les certificats du serveur.
- [ ] Stockez les identifiants dans un coffre-fort sécurisé.
- [ ] Conservez les fichiers
.envuniquement en local, et injectez les secrets dans la plateforme cible dans des environnements partagés. - [ ] Utilisez le mode strict TDS 8.0 pour Azure SQL.
Sécurité des requêtes
- [ ] Utilisez toujours des requêtes paramétrées.
- [ ] Validez les identifiants dynamiques.
- [ ] Utilisez des procédures stockées pour la logique complexe.
- [ ] Limitez la taille des résultats des requêtes.
Sécurité des données
- [ ] Utilisez la protection des données au niveau serveur (masquage, sécurité au niveau des lignes).
- [ ] Utilisez la sécurité au niveau des rangées lorsque cela est approprié.
- [ ] Masquer les données sensibles dans les logs.
Contrôle d’accès
- [ ] Appliquez le principe du moindre privilège.
- [ ] Comptes de lecture et d’écriture distincts.
- [ ] Auditez régulièrement les autorisations.
- [ ] Implémentez des délais d’expiration de connexion.
Monitoring
- [ ] Enregistrez les événements de sécurité.
- [ ] Surveillez les anomalies.
- [ ] Mettez en place des alertes en cas de défaillance.
- [ ] Effectuez des contrôles de sécurité réguliers.