Augmenter la taille d’envoi PHP et trouver la limite bloquante
Pour accepter un fichier de 20 MiB, définissez upload_max_filesize = 20M, donnez plus de marge à la requête
multipart complète avec post_max_size = 25M, et assurez-vous que votre application et votre serveur web
l’autorisent aussi. Modifier les paramètres de PHP ne peut pas outrepasser une limite plus petite dans
votre gestionnaire de téléversement. Ce guide montre comment trouver les paramètres actifs et
reproduire chaque échec via HTTP avec un petit point de terminaison local.
Comment vérifier la taille maximale de téléversement en PHP
Vérifiez le processus PHP qui sert l’URL de téléversement. php --ini indique la configuration en
ligne de commande ; elle ne permet pas de savoir ce que FPM ou Apache a chargé. PHP peut utiliser
des fichiers de configuration différents selon l’interface serveur.
Enregistrez ceci sous check_upload_size.php à côté de votre point de terminaison de téléversement et
appelez-le via le même site web. Sur un site existant, réservez-en l’accès aux administrateurs, puis
supprimez-le après le diagnostic : les chemins de fichiers vous sont utiles mais ne devraient pas
être publics.
<?php
header('Content-Type: application/json');
echo json_encode([
'version' => PHP_VERSION,
'sapi' => PHP_SAPI,
'loaded_ini' => php_ini_loaded_file(),
'scanned_ini' => php_ini_scanned_files(),
'file_uploads' => ini_get('file_uploads'),
'upload_max_filesize' => ini_get('upload_max_filesize'),
'post_max_size' => ini_get('post_max_size'),
'max_file_uploads' => ini_get('max_file_uploads'),
], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
PHP documente des valeurs par défaut de 2M pour upload_max_filesize, 8M pour post_max_size et 20 pour
max_file_uploads ; les installations peuvent les remplacer. Les deux premières directives sont des
limites en octets, tandis que max_file_uploads limite le nombre de fichiers. Dans la notation de taille de
PHP, 20M signifie 20 × 1 024 × 1 024 octets, soit 20 MiB. Consultez les
directives principales et
l’analyseur de tailles.
| Couche | Ce qu’elle limite | Ce qu’il faut vérifier |
|---|---|---|
| Serveur web ou proxy | Le corps de la requête entrante | Un rejet avant l’exécution du gestionnaire PHP ; consultez les journaux de ce serveur |
post_max_size | Le corps POST entier, y compris les champs et la surcharge multipart | $_POST et $_FILES peuvent tous deux être vides |
upload_max_filesize | Chaque fichier téléversé | UPLOAD_ERR_INI_SIZE dans le champ error du fichier |
| Validation applicative | Le fichier que votre application acceptera | Une constante du gestionnaire ou une règle du framework qui peut être inférieure à la limite de PHP |
PHP documente le comportement des superglobales vides
et les codes d’erreur de téléversement.
Un $_FILES vide ne prouve pas à lui seul une requête trop volumineuse : un champ de fichier manquant peut aussi le produire.
Comment augmenter la taille de téléversement en PHP
Modifiez la configuration utilisée par la requête web, en vous servant de la sortie de diagnostic
ci-dessus pour la localiser. Sur Ubuntu, une installation empaquetée de PHP 8.3 utilise couramment
/etc/php/8.3/fpm/php.ini pour FPM ou /etc/php/8.3/apache2/php.ini pour mod_php. Le chemin indiqué et les valeurs effectives
priment sur ces exemples.
file_uploads = On
upload_max_filesize = 20M
post_max_size = 25M
display_errors = Off
log_errors = On
Rechargez le service qui exécute PHP après avoir modifié sa configuration. Avec FPM, il s’agit du
service FPM concerné ; avec mod_php, il s’agit d’Apache. Appelez de nouveau l’URL de diagnostic et
vérifiez les valeurs effectives. Ces deux directives de taille sont des paramètres INI_PERDIR, donc
ini_set() dans upload.php ne peut pas les augmenter. Désactiver l’affichage des erreurs dans la
configuration permet aussi de garder hors de la réponse JSON les avertissements émis pendant
l’analyse de la requête.
Utiliser la configuration PHP-FPM
Un pool peut remplacer php.ini. Si les valeurs diffèrent encore, vérifiez le pool qui sert ce
site. Pour une limite de 20 MiB, les paramètres de pool correspondants sont
php_admin_value[upload_max_filesize] = 20M et php_admin_value[post_max_size] = 25M.
La référence de configuration de FPM
explique les remplacements au niveau du pool. Rechargez le service FPM concerné et vérifiez de
nouveau via HTTP.
Utiliser .htaccess (pour Apache 2.4+ avec mod_php)
Pour PHP exécuté comme module Apache, php_value upload_max_filesize 20M et
php_value post_max_size 25M peuvent être placés dans .htaccess lorsque le serveur autorise ces remplacements.
Ce ne sont pas des paramètres PHP-FPM ; ne les ajoutez pas simplement parce qu’Apache est placé
devant le site. Consultez les instructions de configuration Apache de PHP.
Si la requête n’atteint jamais PHP, vérifiez la limite de corps en amont. La directive Nginx
client_max_body_size
vaut 1m par défaut et renvoie 413 pour une requête trop volumineuse. Pour cet exemple,
client_max_body_size 25m; permet la requête multipart prévue. Apache dispose de
LimitRequestBody, et un proxy
d’hébergement peut imposer une autre limite. Prévoyez de la place pour la requête entière à chaque
couche. Augmenter une limite PHP ne modifie aucun de ces paramètres.
Implémenter le téléversement de fichiers en PHP
Utilisez un environnement Linux local avec PHP 8.3 ou ultérieur en 64 bits, l’extension Fileinfo et cURL. L’exemple a été testé avec PHP 8.3.30 et 8.5.10. Le serveur intégré de PHP est destiné au développement local. Il ne reproduit pas un déploiement FPM, Apache ou proxy.
Créez un nouveau répertoire. Les commandes enchaînées s’arrêtent si le répertoire existe déjà ou si la navigation échoue ; choisissez un autre nom plutôt que d’écraser un projet existant.
mkdir php-upload-demo &&
cd php-upload-demo &&
mkdir public private &&
chmod 700 private
Ne continuez qu’une fois cette opération réussie. Enregistrez le bloc INI ci-dessus sous php-upload.ini
dans ce répertoire. Enregistrez le script de diagnostic sous public/check_upload_size.php.
Script de téléversement PHP
Enregistrez ce gestionnaire complet sous public/upload.php. APP_MAX_BYTES est la limite indépendante de
l’application ; ici, elle correspond à la limite par fichier de 20 MiB de PHP. Le gestionnaire
accepte les fichiers JPEG, PNG et PDF, vérifie que le type MIME détecté correspond à l’extension du
nom de fichier et conserve les octets acceptés sous private/, en dehors de la racine des
documents.
<?php
declare(strict_types=1);
const APP_MAX_BYTES = 20 * 1024 * 1024;
header('Content-Type: application/json');
function rejectUpload(int $status, string $code, string $message): never {
http_response_code($status);
echo json_encode([
'success' => false,
'code' => $code,
'message' => $message,
], JSON_THROW_ON_ERROR);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Allow: POST');
rejectUpload(405, 'method', 'Send a multipart POST request.');
}
$postLimit = ini_parse_quantity(ini_get('post_max_size'));
$contentLength = $_SERVER['CONTENT_LENGTH'] ?? null;
if ($postLimit > 0 && $contentLength !== null && (int) $contentLength > $postLimit) {
rejectUpload(413, 'post_max_size', 'The complete request exceeds PHP post_max_size.');
}
$file = $_FILES['file'] ?? null;
if (!is_array($file) || array_keys($_FILES) !== ['file'] ||
!isset($file['error'], $file['size'], $file['name'], $file['tmp_name']) ||
!is_int($file['error']) || !is_int($file['size']) ||
!is_string($file['name']) || !is_string($file['tmp_name'])) {
rejectUpload(400, 'invalid_upload', 'Send one file in the file field, without array brackets.');
}
if ($file['error'] !== UPLOAD_ERR_OK) {
[$status, $code, $message] = match ($file['error']) {
UPLOAD_ERR_INI_SIZE => [413, 'upload_max_filesize', 'The file exceeds PHP upload_max_filesize.'],
UPLOAD_ERR_FORM_SIZE => [413, 'form_limit', 'The file exceeds the submitted MAX_FILE_SIZE.'],
UPLOAD_ERR_PARTIAL => [400, 'partial_upload', 'The file arrived incomplete. Retry the upload.'],
UPLOAD_ERR_NO_FILE => [400, 'missing_file', 'Choose a file to upload.'],
default => [500, 'upload_unavailable', 'The upload service is unavailable.'],
};
rejectUpload($status, $code, $message);
}
if (!is_uploaded_file($file['tmp_name'])) {
rejectUpload(400, 'invalid_upload', 'The file is not a valid HTTP upload.');
}
$directory = null;
$destination = null;
try {
$size = filesize($file['tmp_name']);
if ($size === false) {
throw new RuntimeException('Cannot measure upload');
}
if ($size === 0) {
rejectUpload(400, 'empty_file', 'The file is empty.');
}
if ($size > APP_MAX_BYTES) {
rejectUpload(413, 'application_limit', 'The file exceeds the application size limit.');
}
$allowed = [
'image/jpeg' => ['jpg', 'jpeg'],
'image/png' => ['png'],
'application/pdf' => ['pdf'],
];
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!isset($allowed[$mime]) || !in_array($extension, $allowed[$mime], true)) {
rejectUpload(415, 'file_type', 'Send a JPEG, PNG, or PDF with a matching extension.');
}
$hash = hash_file('sha256', $file['tmp_name']);
if ($hash === false) {
throw new RuntimeException('Cannot hash upload');
}
$id = bin2hex(random_bytes(16));
$candidate = dirname(__DIR__) . '/private/' . $id;
// Creating a new directory reserves this ID without overwriting an earlier upload.
if (!@mkdir($candidate, 0700)) {
throw new RuntimeException('Cannot reserve storage');
}
$directory = $candidate;
$destination = $directory . '/file.' . $allowed[$mime][0];
if (!@move_uploaded_file($file['tmp_name'], $destination) || !@chmod($destination, 0600)) {
throw new RuntimeException('Cannot store upload');
}
http_response_code(201);
echo json_encode([
'success' => true,
'id' => $id,
'type' => $mime,
'size' => $size,
'sha256' => $hash,
], JSON_THROW_ON_ERROR);
} catch (Throwable $error) {
if ($destination !== null) {
@unlink($destination);
}
if ($directory !== null) {
@rmdir($directory);
}
error_log('Upload failed: ' . get_class($error));
rejectUpload(500, 'upload_unavailable', 'The upload service is unavailable.');
}
La vérification de la taille de requête utilise Content-Length pour distinguer un corps dont la taille
excessive est connue d’un fichier manquant. Les requêtes cURL ci-dessous fournissent cette longueur.
Sans elle, le gestionnaire ne peut pas diagnostiquer ainsi un dépassement de post_max_size ; imposez
une limite de corps dans le serveur web pour les clients qui utilisent des requêtes en streaming.
ini_parse_quantity() gère les suffixes de taille de PHP, y compris une limite nulle, qui désactive le plafond
de taille POST multipart.
Démarrez le serveur depuis php-upload-demo. Choisissez un port disponible et utilisez le même port
dans les requêtes suivantes. Cette commande sert uniquement public/ :
php -c php-upload.ini -S 127.0.0.1:8080 -t public
Laissez ce terminal ouvert. Dans un second terminal, ouvrez le même répertoire de projet et vérifiez :
curl -fsS http://127.0.0.1:8080/check_upload_size.php
Attendez-vous à ce que sapi vaille cli-server, upload_max_filesize vaille 20M et post_max_size vaille 25M.
Le chemin INI chargé doit pointer vers ce projet. Arrêtez le serveur avec Ctrl+C une fois terminé.
Envoyer de vraies requêtes multipart
Pour une sonde de taille reproductible, enregistrez ceci sous make-probe.php à la racine du projet. Il
crée un PNG valide d’un pixel, complété jusqu’à la taille en octets demandée. Ce remplissage est
délibéré : il teste les limites en octets sans nécessiter une grande photo, et montre pourquoi la
détection MIME n’est pas un verdict de sécurité. Le mode de création exclusive refuse d’écraser une
sonde existante.
<?php
declare(strict_types=1);
$bytes = filter_var($argv[2] ?? '', FILTER_VALIDATE_INT);
if ($argc !== 3 || $bytes === false || $bytes < 1024 || $bytes > 30 * 1024 * 1024) {
fwrite(STDERR, "Usage: php make-probe.php OUTPUT BYTES (1024 to 31457280)\n");
exit(1);
}
$png = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg==', true);
$output = @fopen($argv[1], 'xb');
if ($output === false) {
fwrite(STDERR, "Cannot create probe; choose a new output filename.\n");
exit(1);
}
if (fwrite($output, $png) !== strlen($png) || !ftruncate($output, $bytes) || !fclose($output)) {
fwrite(STDERR, "Cannot finish probe.\n");
exit(1);
}
Créez et envoyez un fichier de 6 MiB. -F fournit l’encodage multipart et le délimiteur ;
ne définissez pas vous-même l’en-tête Content-Type. -i affiche le statut de la réponse. Ces
appels de diagnostic omettent l’option --fail de cURL afin que vous puissiez lire le corps JSON
des réponses d’erreur attendues.
php make-probe.php probe-6.png 6291456 &&
curl -sS -i -F 'file=@probe-6.png' http://127.0.0.1:8080/upload.php
Attendez-vous à un HTTP 201 avec success: true, type: "image/png", size: 6291456, un id aléatoire et un
sha256. Les octets restent dans private/<id>/file.png ; il n’existe aucune URL de téléchargement
publique. Comparez l’empreinte de la réponse avec une empreinte locale de la source :
php -r 'echo hash_file("sha256", "probe-6.png"), PHP_EOL;'
Recommencez avec la limite exacte de 20 MiB, puis avec des fichiers assez volumineux pour déclencher chaque limite PHP :
php make-probe.php probe-20.png 20971520 &&
curl -sS -i -F 'file=@probe-20.png' http://127.0.0.1:8080/upload.php
php make-probe.php probe-21.png 22020096 &&
curl -sS -i -F 'file=@probe-21.png' http://127.0.0.1:8080/upload.php
php make-probe.php probe-26.png 27262976 &&
curl -sS -i -F 'file=@probe-26.png' http://127.0.0.1:8080/upload.php
| Fichier | Résultat attendu avec les paramètres documentés |
|---|---|
| PNG de 6 MiB | 201 ; stocké avec des octets et une empreinte identiques |
| PNG de 20 MiB | 201 ; la limite par fichier est inclusive |
| PNG de 21 MiB | 413 avec code: "upload_max_filesize" |
| PNG de 26 MiB | 413 avec code: "post_max_size" ; la requête complète est trop volumineuse |
Pour isoler la limite de l’application, arrêtez le serveur et redémarrez-le avec ce remplacement temporaire :
php -c php-upload.ini -d upload_max_filesize=24M -S 127.0.0.1:8080 -t public
Renvoyez la sonde existante de 21 MiB :
curl -sS -i -F 'file=@probe-21.png' http://127.0.0.1:8080/upload.php
Attendez-vous maintenant à un 413 avec code: "application_limit" : PHP autorise le fichier, mais APP_MAX_BYTES le
rejette. Rétablissez ensuite la commande de serveur d’origine. Si une application existante rejette
encore un fichier de 6 MiB après que vous avez augmenté les limites de PHP, cherchez une limite
applicative plus petite, comme 5242880 octets, ou une règle de validation du framework. Modifier
php.ini ne met pas à jour cette règle.
Les commandes de sonde ne remplacent jamais une sonde existante. Pour en renvoyer une, exécutez uniquement sa commande cURL. Chaque requête réussie crée un nouveau téléversement privé, même si le contenu est identique ; les échecs ne laissent aucun téléversement conservé. La démo conserve les téléversements réussis jusqu’à ce que vous supprimiez ses données privées.
Bonnes pratiques pour des téléversements de fichiers sécurisés
Sécurité des répertoires
Conservez private/ en dehors du répertoire servi, avec le compte PHP comme propriétaire. Chaque
répertoire de téléversement réservé a le mode 0700 ; les fichiers stockés ont le mode 0600.
Le nom de fichier d’origine ne devient jamais un chemin de stockage. La fonction
move_uploaded_file() de PHP
vérifie que la source a été téléversée via PHP, mais elle peut écraser une destination existante.
Réserver un nouveau répertoire aléatoire avant de déplacer le fichier évite cet écrasement dans cet
exemple.
Avant d’exposer un point de terminaison de téléversement
Cet exemple local ne comporte ni authentification, ni quotas par utilisateur, ni analyse antimalware, ni diffusion publique des fichiers. Fileinfo identifie un type probable à partir des octets ; ni son résultat ni une empreinte SHA-256 n’établissent qu’un fichier est inoffensif. Pour une application déployée, décidez qui peut téléverser, limitez l’utilisation du stockage de chacun, et validez ou analysez le contenu avant de le diffuser. Les frameworks et les affichages de progression du téléversement restent soumis aux mêmes limites de requête et de fichier ; ils ne les suppriment pas.
