Vérifier les envois web avec Magika et une API Python
Un fichier nommé photo.jpg peut contenir un PDF. Pour vérifier le type d’un fichier envoyé,
transmettez ses octets à l’API Python de Magika et appliquez
une liste d’autorisation à l’étiquette renvoyée. Ce tutoriel construit une application Flask locale qui
sert à la fois la page d’envoi et son point de terminaison de classification, avec un retour qui suit
la sélection en cours.
Le navigateur envoie le fichier complet à Python. La classification a lieu sur ce serveur, même lorsque les deux processus s’exécutent sur votre ordinateur portable. Un type accepté n’établit pas qu’un fichier est valide ou exempt de logiciels malveillants.
Déterminer ce que l’étiquette peut vous apprendre
Les extensions et les types MIME fournis par le client décrivent ce qu’un fichier prétend être. Les détecteurs de contenu inspectent plutôt les octets : renommer un fichier ne modifie donc pas leur entrée. Magika utilise un modèle entraîné pour distinguer les formats binaires et textuels ; il peut malgré tout mal classer un fichier.
L’API Python expose
identify_bytes(), un indicateur de succès dans result.ok et la prédiction finale dans result.output.
Utilisez result.output.label pour la liste d’autorisation. Le mode HIGH_CONFIDENCE par défaut peut remplacer une
prédiction du modèle à faible confiance par une étiquette générique, que cet exemple rejette avec
les autres types absents de la liste d’autorisation.
Magika et les détecteurs fondés sur les signatures répondent à une question d’identification de format. Ni cette réponse ni un score de confiance élevé ne prouvent qu’un décodeur acceptera le fichier entier. Les limitations connues de Magika excluent également une détection fiable des fichiers polyglottes : un fichier peut être interprétable dans plus d’un format.
Intégrer Magika dans une application pour navigateur
Utilisez Linux, Python 3.12 avec venv et pip, ainsi qu’un navigateur à jour. L’exemple fixe les versions
Magika 0.6.1 et Flask 3.1.0 ; l’installation nécessite un accès à Internet. Une fois installé, Magika
charge localement son modèle intégré : la classification ne nécessite donc aucun service cloud. Les
commandes ci-dessous utilisent Bash.
1. Installer Magika
Collez ceci dans un terminal, depuis un répertoire où vous souhaitez créer un nouveau projet magika-upload. Le
script refuse de réutiliser un répertoire existant et s’arrête si une étape de configuration échoue.
Les parenthèses maintiennent votre shell dans son répertoire d’origine.
(
mkdir magika-upload &&
cd magika-upload &&
mkdir static &&
python3.12 -m venv .venv &&
.venv/bin/python -m pip install magika==0.6.1 flask==3.1.0
)
Ne continuez qu’une fois l’installation réussie. En cas d’échec, examinez l’erreur, puis réessayez dans un nouveau répertoire ou réparez l’environnement incomplet avant de poursuivre. Ne supprimez pas un projet existant pour relancer la configuration.
2. Créer une API de vérification minimale (Flask)
Enregistrez ceci sous magika-upload/app.py. Le modèle se charge une seule fois, au démarrage de l’application.
La route / sert la page que vous allez créer ensuite ; Flask sert aussi automatiquement le
répertoire static.
from flask import Flask, jsonify, request, send_from_directory
from magika import Magika
from werkzeug.exceptions import RequestEntityTooLarge
app = Flask(__name__)
MAX_FILE_BYTES = 5 * 1024 * 1024
app.config['MAX_CONTENT_LENGTH'] = 6 * 1024 * 1024
ALLOWED_TYPES = {'pdf', 'jpeg', 'png'}
magika = Magika()
@app.get('/')
def index():
return send_from_directory(app.root_path, 'index.html')
@app.errorhandler(RequestEntityTooLarge)
def request_too_large(error):
return jsonify(error='Upload is too large'), 413
@app.post('/verify')
def verify_file():
if 'file' not in request.files:
return jsonify(error='No file provided'), 400
content = request.files['file'].read(MAX_FILE_BYTES + 1)
if not content:
return jsonify(error='File is empty'), 400
if len(content) > MAX_FILE_BYTES:
return jsonify(error='Upload is too large'), 413
result = magika.identify_bytes(content)
if not result.ok:
return jsonify(error='File analysis failed'), 500
if result.output.label not in ALLOWED_TYPES:
return jsonify(error='Only PDF, JPEG, and PNG files are allowed'), 415
return jsonify(
file_type=result.output.label,
mime_type=result.output.mime_type,
)
La limite par fichier est de 5 MiB. La limite distincte de 6 MiB par requête laisse de la place aux
en-têtes multipart tout en bornant la requête entière. Comme l’explique le
guide de Flask sur l’envoi de fichiers,
l’analyse des fichiers envoyés peut utiliser un stockage temporaire sur disque. Cette application
n’enregistre pas les fichiers acceptés de façon permanente ; elle lit au plus 5 MiB plus un octet dans
content avant la classification.
3. Relier le front-end
Enregistrez ceci sous magika-upload/index.html :
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Classify a file with Magika</title>
<script src="/static/verify.js" defer></script>
</head>
<body>
<h1>Classify a file with Magika</h1>
<p>PDF, JPEG, or PNG, up to 5 MiB. The file is sent to the local Python server.</p>
<form id="uploadForm" action="/verify" method="post" enctype="multipart/form-data">
<label for="fileInput">File to classify</label>
<input id="fileInput" name="file" type="file">
<button type="submit">Classify file</button>
</form>
<p id="result" role="status">Choose a file, then classify it.</p>
</body>
</html>
Enregistrez le code suivant sous magika-upload/static/verify.js. Modifier la sélection efface immédiatement le résultat
précédent. Une nouvelle soumission rend obsolète la requête précédente : une réponse lente ne peut
donc pas remplacer le retour destiné à un travail plus récent.
const form = document.getElementById('uploadForm')
const input = document.getElementById('fileInput')
const result = document.getElementById('result')
const errors = {
400: 'Choose a non-empty file.',
413: 'File exceeds the upload limit.',
415: 'Only PDF, JPEG, and PNG files are allowed.',
}
let requestId = 0
let activeRequest
input.addEventListener('change', () => {
requestId += 1
activeRequest?.abort()
result.textContent = 'Selection changed. Click Classify file to check it.'
})
form.addEventListener('submit', async (event) => {
event.preventDefault()
const id = ++requestId
activeRequest?.abort()
const file = input.files[0]
if (!file) {
result.textContent = 'Choose a file, then classify it.'
return
}
const controller = new AbortController()
activeRequest = controller
const timeout = setTimeout(() => controller.abort(), 30_000)
const data = new FormData()
data.append('file', file)
result.textContent = `Classifying ${file.name}…`
try {
const response = await fetch('/verify', {
method: 'POST',
body: data,
signal: controller.signal,
})
if (id !== requestId) return
if (!response.ok) {
result.textContent = errors[response.status] ??
`Classification failed (HTTP ${response.status}).`
return
}
const analysis = await response.json()
if (id !== requestId) return
result.textContent = `${file.name}: ${analysis.file_type} (${analysis.mime_type}). ` +
'Accepted by the type allowlist.'
} catch {
if (id !== requestId) return
result.textContent = controller.signal.aborted
? 'Request timed out. Click Classify file to retry.'
: 'Could not reach or read the server. Check the terminal, then retry.'
} finally {
clearTimeout(timeout)
}
})
L’identifiant de requête protège à la fois la réponse et son corps JSON lu de façon asynchrone.
AbortController
cesse d’attendre les requêtes obsolètes, et un minuteur de 30 secondes borne chaque tentative du
navigateur. Interrompre la requête du navigateur ne garantit pas que Python cesse de traiter les octets
qu’il a déjà reçus.
Depuis le même répertoire parent que celui utilisé pour la configuration, démarrez l’application :
(
cd magika-upload &&
.venv/bin/python -m flask --app app run --host 127.0.0.1 --port 5050
)
Attendez le message de démarrage de Flask, puis ouvrez http://127.0.0.1:5050/. Ouvrez cette URL plutôt que le
fichier HTML sur le disque : la page, le script et le point de terminaison /verify doivent partager la même
origine. Sélectionnez un fichier avec
File to classify, puis cliquez sur
Classify file. Vous pouvez cliquer de nouveau sur le bouton pour réessayer avec le
même fichier. Arrêtez le serveur avec Ctrl+C une fois terminé.
4. Comparer les résultats à une liste d’autorisation
Essayez un petit fichier PNG valide renommé en photo.txt. Vous devriez voir
photo.txt: png (image/png). Accepted by the type allowlist.
Le nom de fichier n’autorise pas l’envoi : seules les étiquettes pdf, jpeg et png du serveur l’autorisent.
Pour les fichiers JPEG, l’étiquette est jpeg, même lorsque le nom de fichier se termine par .jpg.
Essayez ensuite un fichier texte non vide renommé en photo.png. Il devrait produire
Only PDF, JPEG, and PNG files are allowed.
Essayez aussi un fichier vide et un fichier dépassant légèrement 5 MiB. Ils sont rejetés avant
l’exécution de Magika. Pendant qu’une requête est en attente, sélectionnez un autre fichier : la page
devrait effacer le résultat en attente et attendre que vous classiez la nouvelle sélection.
Résoudre l’échec d’une requête
- L’installation ou le chargement du modèle échoue : lisez d’abord l’erreur dans le terminal.
Vérifiez que l’installation s’est terminée dans
.venvet que vous lancez bien son exécutable Python. La page ne peut pas se charger tant que l’application n’a pas démarré correctement. - Le port est occupé : Flask se ferme avec une erreur d’adresse déjà utilisée. Choisissez un autre port dans la commande de démarrage et ouvrez l’URL correspondant à ce port. Il s’agit d’un serveur de développement local, pas d’une configuration de déploiement.
- Le serveur rejette la requête : HTTP 400 signifie un fichier manquant ou vide, 413 un dépassement de limite de taille, et 415 une étiquette hors de la liste d’autorisation. Des requêtes plus volumineuses peuvent provoquer une réinitialisation de la connexion sur le serveur de développement au lieu d’une réponse 413 lisible.
- Le navigateur ne peut pas lire une réponse : vérifiez le terminal du serveur et l’URL, puis réessayez. Les échecs HTTP, un serveur arrêté et les délais d’expiration ne valent pas acceptation. La page affiche des messages d’erreur fixes au lieu d’afficher une page d’erreur du serveur ou une trace d’appels.
Utiliser le résultat dans un flux d’envoi de fichiers
Si vous étendez cette application pour stocker des fichiers, effectuez la vérification par liste d’autorisation et le stockage sur les mêmes octets reçus. Une réponse de classification réussie ne doit pas devenir une autorisation pour un envoi ultérieur non vérifié. L’exemple renvoie uniquement une classification ; il ne crée ni envoi enregistré ni approbation réutilisable.
Une étiquette autorisée peut aider à choisir un décodeur d’images, un analyseur de documents ou un service de modération en aval. Ces opérations nécessitent toujours leurs propres validations et limites de ressources. L’analyse antimalware est une tâche distincte, et une prédiction de format ne doit jamais être présentée comme un verdict de sécurité.
Pour un flux d’envoi géré, consultez la référence sur la vérification de fichiers (English). Conservez la même distinction quelle que soit la conception : reconnaître un format est l’une des décisions du traitement d’un envoi, et non la preuve que son contenu est sûr.
