Sécuriser les URLs des fichiers hébergés
Que nous utilisions un hébergement de fichiers en base ou via des buckets S3, il faut garantir que les URLs de nos fichiers sensibles soient éphémères. Même si vous avez un identifiant de fichier aléatoire (UUID) et que vous estimez qu'on ne peut pas deviner l'URL :
- L'URL peut être gardée dans l'historique de navigation ;
- L'utilisateur légitime peut l'avoir stockée dans un autre endroit visible par d'autres ;
- L'utilisateur peut ne plus être légitime pour voir ce fichier.
En ajoutant une durée de validité à l'URL du fichier vous garantissez que le fichier ne sera vu que dans le contexte où l'utilisateur aura communiqué avec l'API. Bien entendu, pour des fichiers publics qui peuvent être indexés (logos…), il n'y a aucune raison d'y ajouter une sécurité.
L'idée est de donner une validité à notre URL en y ajoutant en paramètre une signature (générée par chiffrement). Imaginons que nous sommes dans une SPA (les appels API se font en asynchrone du chargement de page) :
- L'utilisateur va sur
monsite.com/compte
; - Le frontend demande à l'API de récupérer l'objet utilisateur ;
- L'API :
- Récupère l'utilisateur en base de données et voit que la propriété
private_photo
est une pièce jointe qui a l'identifiant 123 ; - Passe l'identifiant dans un petit helper qui va d'abord générer l'url finale
monsite.com/fichier?id=123
; - Puis générer un JWT avec comme payload
urn:claim:file_id = 123
et une validité de 10 minutes pour ensuite l'ajouter en paramètre pour donnermonsite.com/fichier?id=123?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cm46Y2xhaW06ZmlsZV9pZCI6IjEyMyIsImlhdCI6MTUxNjIzOTAyMn0.xpP88WITmYZ-F0l1iL_VbAcc-PBU53xeE4Dnm-M9Pj4
; - Renvoie l'objet utilisateur avec pour
private_photo
l'URL finale.
- Récupère l'utilisateur en base de données et voit que la propriété
C'est très schématisé mais c'est l'idée. Ainsi le endpoint /fichier
a juste à vérifier la validité du JWT avec n'importe quelle librairie JWT classique.
- Si vous utilisez les buckets S3 : certains providers de buckets S3 ont cette fonctionnalité de signature d'URL, il faut donc que votre backend se charge de signer l'URL avec la librairie cliente du provider ;
- Sinon si vous êtes en base de données il faut créer vous-même ce petit helper.
Le bénéfice d'avoir la génération de l'URL signée quand on récupère la donnée métier est que vous passez forcément par les étapes de vérification des droits d'accès. Vous évitez ainsi de centraliser les droits d'accès au niveau de la table de fichiers (qui ont des risques d'être désynchronisés avec les vrais entités métiers).
Quelques points importants (que ce soit en base ou via S3 directement) :
- La validité du JWT n'a pas besoin d'être très longue puisque aussitôt le frontend récupère les données, aussitôt il va charger le fichier (le fichier peut être gardé en cache) ;
- En l'état actuel des choses, la mise en cache ne va pas marcher car le JWT est différent à chaque affichage de page. La technique est de rendre fixe l'URL sur un intervalle donné (ce n'est pas parfait, mais ça fait le travail) :
- Au moment de générer le JWT on spécifie la date d'expiration en utilisant un modulo (de 10 minutes par exemple) ;
- Les expirations se font donc à des "heures fixes" (1h10, 1h20, 1h30…) ;
- Assurez-vous quand même sur votre endpoint de renvoyer le header HTTP
Cache-Control
afin d'adapter le contexte et de ne pas garder localement des images dont la validité a expiré :- Pour un fichier public avec la valeur :
public, immutable, no-transform, max-age=0
- Pour un fichier privé avec la valeur :
private, max-age=${minutesToSeconds(15)}
.
- Pour un fichier public avec la valeur :
Désolés si l'ensemble paraît être un peu de travail, mais c'est le nécessaire pour sécuriser proprement les fichiers (l'utilisation d'un S3 prend en charge quelques étapes mais laisse reposer sur vous l'architecture de sécurisation ainsi que sa mise en place).