Säkerhetsbästa praxis för mssql-python-applikationer

Säkra dina mssql-python-applikationer genom att följa dessa bästa praxis för autentisering, parameteriserade frågor och dataskydd.

Börja med lösenordslös autentisering när du kan. Behandla lokala .env filer och SQL-lösenord som tillfälliga utvecklingshjälpmedel och flytta hemligheter till hanterade identiteter eller en hemlig lagring innan koden når en delad miljö.

Autentiseringssäkerhet

Använd Microsoft Entra-autentisering istället för SQL-autentisering

Microsoft Entra-autentisering eliminerar lagrade lösenord och stödjer hanterade identiteter. Föredrar det framför SQL-autentisering i alla miljöer.

För arbetsbelastningar som finns i Azure använder du en hanterad identitet med ActiveDirectoryMSI. Den behöver inga lagrade hemligheter och ansluter utan att gå igenom en legitimationskedja:

import mssql_python

def connect_with_managed_identity():
    return mssql_python.connect(
        "Server=<server>.database.windows.net;"
        "Database=<database>;"
        "Authentication=ActiveDirectoryMSI;"
        "Encrypt=yes;"
    )

För lokal utveckling, använd ActiveDirectoryDefault, som automatiskt plockar upp din Azure CLI eller andra utvecklaruppgifter. Undvik det i produktion, eftersom DefaultAzureCredential provar varje autentiseringsuppgiftsprovider i tur och ordning vid den första anslutningen, vilket ger extra latens som arbetsbelastningar i produktion inte behöver:

import mssql_python

def connect_with_default_credential():
    return mssql_python.connect(
        "Server=<server>.database.windows.net;"
        "Database=<database>;"
        "Authentication=ActiveDirectoryDefault;"
        "Encrypt=yes;"
    )

Undvik: SQL-autentisering lagrar uppgifter i kod/konfiguration och är sårbar för läckor.


conn = mssql_python.connect("Server=...;UID=user;PWD=password")

Hårdkoda aldrig inloggningsuppgifter

Använd miljövariabler endast för lokal utveckling. I delade miljöer föredrar man lösenordslös autentisering. När ett gammalt SQL-autentiseringsflöde är oundvikligt, hämta hemligheten vid körning från en hemlig lagring istället för att kontrollera en fullständig reťazec pripojenia i versionskontroll.

Följande metod hårdkodar inloggningsuppgifter och bör aldrig användas:

conn_str = "Server=<server>;UID=<login>;PWD=<password>"

För lokal utveckling läser du autentiseringsuppgifter från miljövariabler:

import os

conn_str = (
    f"Server={os.environ['DB_SERVER']};"
    f"Database={os.environ['DB_NAME']};"
)

För delade miljöer som fortfarande behöver en hemlighet, hämta den vid körning från 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

SQL-injektionsprevention

Använd alltid parametriserade frågor

Parameteriserade frågor förhindrar SQL-injektion genom att separera användarinmatning från frågestrukturen. Använd alltid parametrar för användarinmatning.

Följande strängformaterade fråga är sårbar för SQL-injektion. Bygg aldrig frågor på det här sättet:

user_input = "'; DROP TABLE Person.Person; --"
cursor.execute(f"SELECT * FROM Person.Person WHERE FirstName = '{user_input}'")

Följande parameteriserade fråga är säker, eftersom drivrutinen skickar värdet separat från frågetexten:

user_input = "'; DROP TABLE Person.Person; --"
cursor.execute(
    "SELECT * FROM Person.Person WHERE FirstName = %(name)s",
    {"name": user_input}
)

Parametrisera alla frågekomponenter

Du kan inte direkt parametrisera tabell- och kolumnnamn. Att interpolera dem från användarinput gör din app sårbar för SQL-injektion:

table = user_input
cursor.execute(f"SELECT * FROM {table}")

Validera istället dynamiska identifierare mot en tillåtningslista av tillåtna värden och parametrisera de återstående värdena.

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

Validera och sanera indata

När du bygger dynamisk SQL med identifierare, validera varje värde mot ett strikt mönster innan du använder det:

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

Använd lagrade procedurer för komplexa operationer

Lagrade procedurer minskar SQL-ytan som exponeras för applikationskoden:

employee_id = 5
cursor.execute("""
    EXECUTE dbo.uspGetEmployeeManagers @BusinessEntityID = %(id)s
""", {"id": employee_id})

Anslutningssäkerhet

Kräver kryptering

Kryptera alltid anslutningar. Azure SQL upprätthåller kryptering som standard. För lokal SQL Server, sätt Encrypt=yes uttryckligen:

conn = mssql_python.connect(
    "Server=<server>.database.windows.net;"
    "Database=<database>;"
    "Encrypt=yes;"
    "TrustServerCertificate=no"
)

Använd TDS 8.0 strikt läge för högsta säkerhet

TDS 8.0 tillhandahåller:

  • TLS 1.3 från anslutningsstart
  • Certifikatvalidering krävs
  • Ingen fallback till äldre protokoll
conn = mssql_python.connect(
    "Server=tcp:<server>.database.windows.net,1433;"
    "Database=<database>;"
    "Encrypt=strict"
)

Validera servercertifikat

Validera alltid servercertifikatet i produktion för att förhindra attacker från angriparen i mitten. Ange inte TrustServerCertificate=yes, eftersom det kringgår valideringen. Ställ TrustServerCertificate=no istället in att validera mot CA-certifikaten och sätt HostNameInCertificate att verifiera värdnamnet:

conn = mssql_python.connect(
    "Server=<server>;"
    "Database=<database>;"
    "Encrypt=yes;"
    "TrustServerCertificate=no;"
    "HostNameInCertificate=<server>.domain.com"
)

Dataskydd

Skydda känslig data på servernivå

Always Encrypted går för närvarande inte att konfigurera via mssql-python reťazec pripojenia-nyckelord. Om du behöver Always Encrypted, använd pyodbc med ODBC-drivrutinen för SQL Server, som stödjer det. Även om de inte ger samma skyddsnivå kan du använda SQL Server-funktioner som dynamisk datamaskering och radsäkerhet för att skydda känsliga kolumner.

employee_id = 1
cursor.execute("""
    SELECT NationalIDNumber, LoginID
    FROM HumanResources.Employee
    WHERE BusinessEntityID = %(id)s
""", {"id": employee_id})

row = cursor.fetchone()

Skydda data under överföring

  • Använd Encrypt=yes i anslutningssträngar.
  • Använd VPN eller privata endpoints för lokala anslutningar.
  • Använd Azure Private Link för Azure SQL.

Princip om minsta privilegium

Använd minimala databasbehörigheter

Applikationskonton bör ha minimala behörigheter. Använd inte sa eller db_owner för applikationsanslutningar.

  • Skrivskyddad rapportering: GRANT SELECT ON SCHEMA::dbo TO ReportingApp;
  • Specifik tabellåtkomst: GRANT SELECT, INSERT, UPDATE ON Orders TO OrderProcessor;
  • Endast lagrad procedur: GRANT EXECUTE ON ProcessOrder TO OrderProcessor;

Använd olika konton för olika operationer

Säkerhetskopiera varje privilegienivå med sin egen identitet så att en läsväg inte kan utföra skrivningar. Detta exempel använder två användartilldelade hanterade identiteter, en beviljad skrivbar åtkomst och en beviljad skrivåtkomst, vald med klient-ID:

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

Håll hemligheter för programroller utanför källkodshanteringen

Lösenord för applikationsroller är fortfarande hemligheter. Lagra dem i ett valv eller en hemlighetsinjicerad miljövariabel och rotera dem med samma omsorg som med andra inloggningsuppgifter.

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

Granskning och loggning

Logga säkerhetshändelser

Logga inte känslig data, men logga säkerhetsrelevanta händelser som misslyckade anslutningar, behörighetsfel och misstänkta frågor.

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

Granska känsliga operationer

Konfigurera databasgranskning för känsliga tabeller och operationer. Du kan också implementera applikationsnivågranskning för kritiska åtgärder.

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

Logga aldrig känslig data

Inkludera inte parametervärden i loggmeddelanden. Logga operationen, inte datan.

Undvik – läcker värdet in i loggen:

nid = "295847284"
logger.debug(f"Query: SELECT * FROM HumanResources.Employee WHERE NationalIDNumber = '{nid}'")

Rekommenderat – loggar avsikt utan att exponera värden:

logger.debug("Executing employee lookup query")
nid = "295847284"
cursor.execute("SELECT * FROM HumanResources.Employee WHERE NationalIDNumber = %(nid)s", {"nid": nid})

Felhantering

Avslöja inte interna detaljer

Logga detaljerade fel internt men returnera generiska meddelanden till användarna för att undvika läcka av databasstrukturen:

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

Sanera felmeddelanden

Kartlägg databasens undantag för användarvänliga meddelanden som inte avslöjar implementeringsdetaljer:

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

Checklista för säkerhet

Checklista för anslutningssäkerhet

  • [ ] Använd Microsoft Entra-autentisering när det är möjligt.
  • [ ] Aktivera kryptering (Encrypt=yes).
  • [ ] Validera servercertifikat.
  • [ ] Förvara inloggningsuppgifter i ett säkert valv.
  • [ ] Behåll .env-filer enbart lokalt, och tillför hemligheter via målplattformen i delade miljöer.
  • [ ] Använd TDS 8.0 strict mode för Azure SQL.

Söksäkerhet

  • [ ] Använd alltid parametriserade frågor.
  • [ ] Validera dynamiska identifierare.
  • [ ] Använd lagrade procedurer för komplex logik.
  • [ ] Begränsa storleken på frågeresultat.

Datasäkerhet

  • [ ] Använd servernivå-dataskydd (maskering, radnivåsäkerhet).
  • [ ] Använd säkerhet på radnivå där det är lämpligt.
  • [ ] Maskera känslig data i loggar.

Åtkomstkontroll

  • [ ] Använd principen om minsta privilegium.
  • [ ] Separata läs-/skrivkonton.
  • [ ] Granska regelbundet tillstånd.
  • [ ] Implementera tidsgränser för anslutningar.

Övervakning

  • [ ] Logga säkerhetshändelser.
  • [ ] Övervaka för avvikelser.
  • [ ] Sätt upp varningar vid fel.
  • [ ] Genomför regelbundna säkerhetskontroller.