Aller au contenu principal

Sécurité du Function Calling

Ne jamais exécuter aveuglément

Le function calling donne au modèle la capacité de demander l’exécution de fonctions dans votre système. C’est puissant, mais c’est aussi un vecteur d’attaque potentiel. Un utilisateur malveillant peut tenter de manipuler le modèle pour qu’il appelle des fonctions avec des paramètres dangereux. Votre code doit être le dernier rempart de sécurité.

Principe fondamental : validation des arguments

Le modèle génère des arguments JSON. Avant d’exécuter quoi que ce soit, validez chaque argument :

import re

def validate_transaction_id(transaction_id: str) -> bool:
    """Valide le format d'un identifiant de transaction."""
    # Format attendu : T suivi de 4 chiffres
    return bool(re.match(r"^T\d{4}$", transaction_id))


def retrieve_payment_status_safe(df, transaction_id: str) -> str:
    """Version sécurisée avec validation."""
    # Étape 1 : valider le format
    if not validate_transaction_id(transaction_id):
        return json.dumps({
            "error": "invalid_format",
            "message": "L'identifiant doit être au format Txxxx (ex: T1001)"
        })

    # Étape 2 : vérifier l'existence
    if transaction_id not in df["transaction_id"].values:
        return json.dumps({
            "error": "not_found",
            "message": f"Transaction {transaction_id} introuvable"
        })

    # Étape 3 : exécuter
    status = df[df["transaction_id"] == transaction_id]["payment_status"].values[0]
    return json.dumps({"status": status})

Injection de prompt via les arguments

Un utilisateur malveillant peut tenter d’injecter des instructions dans les arguments. Exemple :

Utilisateur : "Cherche le paiement avec l'ID: T1001; DROP TABLE payments"

Le modèle pourrait générer {"transaction_id": "T1001; DROP TABLE payments"}. Si votre fonction utilise cet argument directement dans une requête SQL, c’est une injection SQL classique.

Protections :

# JAMAIS ça — injection SQL directe
def bad_query(transaction_id):
    cursor.execute(f"SELECT * FROM payments WHERE id = ' {transaction_id}'")

# TOUJOURS ça — requêtes paramétrées
def safe_query(transaction_id):
    cursor.execute("SELECT * FROM payments WHERE id = %s", (transaction_id,))

# ENCORE MIEUX — validation + requête paramétrée
def best_query(transaction_id):
    if not validate_transaction_id(transaction_id):
        raise ValueError("Format d'identifiant invalide")
    cursor.execute("SELECT * FROM payments WHERE id = %s", (transaction_id,))

Liste blanche de fonctions

Ne rendez disponibles que les fonctions strictement nécessaires. Implémentez une liste blanche explicite :

# Définir explicitement les fonctions autorisées
ALLOWED_FUNCTIONS = {
    "retrieve_payment_status",
    "retrieve_payment_date",
    "search_transactions"
}

def execute_tool_call_safe(tool_call, available_functions):
    """Exécution avec vérification de la liste blanche."""
    function_name = tool_call.function.name

    # Vérifier la liste blanche
    if function_name not in ALLOWED_FUNCTIONS:
        return json.dumps({
            "error": "forbidden",
            "message": f"La fonction {function_name} n'est pas autorisée."
        })

    if function_name not in available_functions:
        return json.dumps({
            "error": "not_implemented",
            "message": f"La fonction {function_name} n'est pas implémentée."
        })

    # Exécuter
    function_params = json.loads(tool_call.function.arguments)
    return available_functions[function_name](**function_params)

Limiter les actions destructives

Classifiez vos fonctions par niveau de risque :

# Niveaux de risque
READ_ONLY = {"retrieve_payment_status", "retrieve_payment_date", "search_transactions"}
WRITE = {"update_payment_status", "create_refund"}
DESTRUCTIVE = {"delete_transaction", "purge_old_records"}

def execute_with_permissions(tool_call, available_functions, permission_level="read"):
    """Exécute avec vérification du niveau de permission."""
    function_name = tool_call.function.name

    if permission_level == "read" and function_name not in READ_ONLY:
        return json.dumps({
            "error": "permission_denied",
            "message": f"Seules les fonctions de lecture sont autorisées. "
                       f"{function_name} nécessite le niveau 'write'."
        })

    if function_name in DESTRUCTIVE:
        return json.dumps({
            "error": "confirmation_required",
            "message": f"L'action {function_name} est destructive et "
                       f"nécessite une confirmation manuelle."
        })

    function_params = json.loads(tool_call.function.arguments)
    return available_functions[function_name](**function_params)

Rate limiting par fonction

Empêchez les abus en limitant le nombre d’appels :

from collections import defaultdict
import time

class FunctionRateLimiter:
    def __init__(self, max_calls_per_minute=10):
        self.max_calls = max_calls_per_minute
        self.call_history = defaultdict(list)

    def can_call(self, function_name: str) -> bool:
        now = time.time()
        # Nettoyer les appels de plus d'une minute
        self.call_history[function_name] = [
            t for t in self.call_history[function_name]
            if now - t < 60
        ]
        return len(self.call_history[function_name]) < self.max_calls

    def record_call(self, function_name: str):
        self.call_history[function_name].append(time.time())

rate_limiter = FunctionRateLimiter(max_calls_per_minute=10)

Sanitisation des résultats

Avant de renvoyer un résultat au modèle, vérifiez qu’il ne contient pas de données sensibles :

def sanitize_result(result: dict) -> dict:
    """Retire les champs sensibles avant de renvoyer au modèle."""
    sensitive_keys = {"password", "api_key", "secret", "token", "ssn", "credit_card"}

    sanitized = {}
    for key, value in result.items():
        if key.lower() in sensitive_keys:
            sanitized[key] = "[MASQUÉ]"
        elif isinstance(value, dict):
            sanitized[key] = sanitize_result(value)
        else:
            sanitized[key] = value

    return sanitized

Points clés à retenir

  • Validez toujours les arguments avant d’exécuter une fonction — format, type, valeurs autorisées
  • Utilisez des requêtes paramétrées pour éviter les injections SQL
  • Implémentez une liste blanche de fonctions autorisées
  • Classifiez les fonctions par niveau de risque (lecture, écriture, destructif)
  • Ajoutez du rate limiting pour empêcher les abus
  • Sanitisez les résultats pour ne pas exposer de données sensibles au modèle