Secure_filename

September 19, 2024

La librairie Python Werkzeug

Werkzeug est une bibliothèque d'outils pour les applications WSGI, en particulier utilisée avec Flask.

Petite aparté sur WSGI

Web Server Gateway Interface (WSGI) est une norme Python qui définit une interface simple entre un serveur et une application web.

Fonctionnement

WSGI sépare le rôle du serveur web de celui de l'application. Le serveur gère les requêtes HTTP et les transmet à l'application. L'application traite la requête et renvoie une réponse.

FastAPI

FastAPI utilise la norme ASGI (Asynchronous Server Gateway Interface), qui permet une meilleure gestion de la concurrence. C'est plus rapide et mieux adapté au contexte actuel du développement web.

werkzeug et FastAPI

Werkzeug est une bibliothèque WSGI. Par conséquent, il n'est pas pertinent de l'utiliser dans une application FastAPI qui repose sur ASGI.

Il est donc temps de dire au revoir à Werkzeug et de recréer la fonction secure_filename.

La fonction secure_filename

https://tedboy.github.io/flask/_modules/werkzeug/utils.html#secure_filename

Cette fonction accepte une chaîne de caractères en paramètre et la transforme, si nécessaire, pour assurer une gestion sécurisée des fichiers.

Les transformations appliquées sont principalement :

Cela permet d'obtenir un nom de fichier facile à manipuler.

Comme illustré dans la documentation, voici quelques exemples :

>>> secure_filename("My cool movie.mov") 
'My_cool_movie.mov' 
>>> secure_filename("../../../etc/passwd") 
'etc_passwd' 
>>> secure_filename(u'i contain cool \xfcml\xe4uts.txt')
'i_contain_cool_umlauts.txt'

Ma première idée était de supprimer tous les caractères spéciaux et de ne garder que les lettres :

def secure_filename(filename: str) -> str: 
	filename = filename.strip().replace(" ", "_")
	filename = re.sub(r'[^a-zA-Z0-9_.-]', '', filename) 
	return filename

Cependant, cela ne fonctionne pas parfaitement, car il faut d'abord séparer le nom du fichier de son extension, puis modifier uniquement la partie du nom. Après un petit prompt pour me faciliter le travail:

def secure_filename(filename: str) -> str:  
    # Strip leading/trailing whitespace and replace problematic characters  
    match = re.match(r"(.+)\.(png|jpg|jpeg)$", filename, re.IGNORECASE)  
    if match is not None:  
        name, ext = match.groups()  
        # Replace space or . by "_"  
        name = re.sub(r"[ \.]", "_", name)        # Remove any path separators or unsafe characters        # 
        name = re.sub(r'[^a-zA-Z0-9_-]', '', name)        # erase all _ at start or at the end of the word        # 
        name = re.sub(r'^_*|_*$', '', name)        
        filename = f"{name}.{ext.lower()}"  
        return filename  
    else:  
        raise HTTPException(status_code=401, detail="Image must have a name and have a .png, .jpeg, .jpg extension")

Mais là encore, ce n'est pas parfait... Comment gérer un fichier nommé "weird!@#$#%!file.png" ? Le résultat devrait être "weird_file.png", mais ce n'est pas le cas avec ce code.

Je dois donc isoler les blocs composés uniquement de caractères alphabétiques, et chaque partie doit être dans un tuple. Grâce à ChatGPT, j'ai découvert la fonction re.findall(). Top, c'est exactement ce dont j'ai besoin. Je me suis ensuite souvenu de la méthode "".join(tuple) qui crée une chaîne de caractères à partir des éléments d'un tuple.

Plus besoin de toutes les manipulations de string, voici donc la version finale :

def secure_filename(filename: str) -> str:  
    # Strip leading/trailing whitespace and replace problematic characters  
    match = re.match(r"(.+)\.(png|jpg|jpeg)$", filename, re.IGNORECASE)  
    if match is not None:  
        name, ext = match.groups()  
        groups = re.findall(r"[a-zA-Z]+", name)  
        name = "_".join(groups)    
        return filename  
    else:  
        raise HTTPException(status_code=401, detail="Image must have a name and have a .png, .jpeg, .jpg extension")

Bravo to myLLM&me