Exécution sécurisée des scripts (safe_exec)
Exécution sécurisée des scripts
Dodock exécute du code saisi dans l'interface : scripts serveur, formats d'impression, modèles d'e-mail. Ce code passe par RestrictedPython, qui bloque les imports et les attributs internes de Python. Il ne voit que les fonctions d'une liste fixe, appelée espace de noms. Dodock utilise deux espaces de noms.
Contexte de rendu : render_safe_globals
Le moteur Jinja (get_jenv) utilise ce contexte pour les formats d'impression, les modèles d'e-mail et les autres modèles. Il donne seulement un accès en lecture :
| Fonction | Résultat |
|---|---|
frappe.get_doc, frappe.get_cached_doc, frappe.get_last_doc | Une copie du document en dictionnaire (SafeDoc), sans save, submit ni delete |
frappe.get_meta | Les métadonnées en dictionnaire, avec get_field, has_field, get_options et get_label |
frappe.get_list, frappe.get_all | Des listes de documents. Les champs qui contiennent une parenthèse (fonctions SQL) sont retirés |
frappe.db.get_value, frappe.db.get_single_value, frappe.db.exists, frappe.db.count | Des valeurs lues en base |
frappe.db.sql | Seulement les requêtes SELECT et EXPLAIN |
frappe.make_get_request | Une requête HTTP GET, avec un contrôle des adresses internes |
frappe.utils, frappe.format_value, frappe.render_template | Le formatage des dates, nombres et devises |
Le contexte de rendu n'a pas frappe.new_doc, frappe.sendmail ni frappe.enqueue.
{% set client = frappe.get_doc("Customer", doc.customer) %}
{{ client.customer_name }}
client.save() échoue dans ce modèle, car SafeDoc n'a pas cette méthode.
Un administrateur du serveur peut désactiver ce contexte avec la clé disable_render_safe_exec du fichier common_site_config.json. Les modèles Jinja reçoivent alors le contexte des scripts serveur. Ne le faites pas sur un site où des utilisateurs non sûrs modifient des modèles.
Contexte d'exécution : exec_safe_globals
Les Scripts serveur utilisent ce contexte. Il reprend le contexte de rendu et remplace les fonctions de lecture par les vraies fonctions de Dodock :
| Fonction | Usage |
|---|---|
frappe.get_doc, frappe.new_doc, frappe.copy_doc, frappe.get_mapped_doc | Documents complets, avec insert, save, submit |
frappe.delete_doc, frappe.rename_doc | Supprimer ou renommer un document |
frappe.db.set_value, frappe.db.commit, frappe.db.rollback | Écrire en base et gérer la transaction |
frappe.sendmail, frappe.get_print, frappe.attach_print | E-mails et impressions |
frappe.enqueue, frappe.is_job_queued | Lancer une méthode whitelistée en arrière-plan |
frappe.call | Appeler une méthode whitelistée |
frappe.make_post_request, make_put_request, make_patch_request, make_delete_request | Requêtes HTTP |
frappe.db.sql reste limité à SELECT et EXPLAIN. Une requête frappe.qb lancée depuis un script serveur doit aussi être un SELECT, sinon Dodock lève une PermissionError.
Dans un script de type Evénement déclencheur (DocType Event), frappe.db.commit et frappe.db.rollback ne sont pas disponibles.
# Script serveur de type API (exemple fictif)
commande = frappe.get_doc("Sales Order", frappe.form_dict.commande)
commande.add_comment("Comment", "Relance envoyée")
frappe.sendmail(
recipients=[commande.contact_email],
subject=f"Commande {commande.name}",
message="Votre commande est en cours de préparation.",
)
Activer les scripts serveur
Les scripts serveur sont désactivés par défaut. Activez-les dans la configuration du bench :
bench set-config -g server_script_enabled 1
Sans cette clé, safe_exec refuse d'exécuter le code et affiche « Server Scripts are disabled ».
Appeler safe_exec depuis une application
from frappe.utils.safe_exec import safe_exec
local_vars = {}
safe_exec("resultat = frappe.db.count('Customer')", _locals=local_vars)
local_vars["resultat"]
La signature est safe_exec(script, _globals=None, _locals=None, *, restrict_commit_rollback=False, script_filename=None). restrict_commit_rollback=True retire commit, rollback et add_index. Pendant l'exécution, frappe.flags.in_safe_exec vaut True.
Pour une expression seule, utilisez safe_eval(code, eval_globals=None, eval_locals=None) du même module.