Größenlimits für Bild-Uploads in PHP
„Upload failed“ bei einem großen Foto kann von fünf verschiedenen Stellen kommen, und in PHP sitzen zwei davon in php.ini und greifen, bevor Ihr Skript läuft. Diese Seite führt jede Ebene in der Reihenfolge auf, in der ein Request auf sie trifft, nennt, was jede zurücksendet, und beschreibt ein Verhalten von PHP, das aus einem sauberen 413 eine HTML-Seite macht, wenn Sie keine Einstellung ändern.
Die Ebenen, der Reihe nach
- Der Galerie-Dialog. Mit
maxUploadFileSize: 4000000in den Editor-Optionen lehnt der Dialog eine größere Datei ab, bevor er sie sendet. Eine Datei mit 4.5 MB ergab „big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB.“ (der Dialog rechnet in binären Megabytes). Das ist eine Komfortfunktion. Der Server kann sich nicht darauf verlassen. - Der Webserver. Laut nginx-Dokumentation ist der Standardwert von
client_max_body_size1m, sodass ein 2-MB-Foto abgelehnt wird, bevor PHP es sieht, mit der eigenen HTML-Seite von nginx für 413. Das Gegenstück in Apache istLimitRequestBody.nginx.conf.exampleim Beispiel setztclient_max_body_size 5m;. post_max_size(standardmäßig 8M) begrenzt den gesamten Request-Body.upload_max_filesize(standardmäßig 2M) begrenzt jede einzelne Datei.- Ihre eigene Prüfung: im Beispiel 4,000,000 Bytes, beantwortet mit einem JSON-413.
Damit das Limit des Beispiels dasjenige ist, das greift, muss jede frühere Ebene etwas mehr als 4,000,000 Bytes zulassen. Die Multipart-Kodierung fügt rund um die Datei einige hundert Bytes hinzu.
upload_max_filesize = 5M
post_max_size = 8M
display_errors = Off
log_errors = OnWie das Skript sie unterscheidet
// Over post_max_size, PHP drops the WHOLE body: $_POST and $_FILES are both empty.
$length = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
if (empty($_FILES) && empty($_POST) && $length > rte_ini_bytes('post_max_size')) {
throw new RteHttpError(413, 'The upload is larger than the server accepts (post_max_size).');
}
$file = $_FILES['file'] ?? null;
// One file only: "file[]" arrives as nested arrays, which this refuses.
if (!is_array($file) || !is_int($file['error'] ?? null) || !is_string($file['tmp_name'] ?? null)) {
throw new RteHttpError(400, 'No file received. Send it as the "file" field.');
}
switch ($file['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_INI_SIZE: // larger than upload_max_filesize
case UPLOAD_ERR_FORM_SIZE: // larger than a MAX_FILE_SIZE form field
throw new RteHttpError(413, 'The image is larger than the server accepts.');
case UPLOAD_ERR_PARTIAL:
throw new RteHttpError(400, 'The upload was interrupted. Please try again.');
case UPLOAD_ERR_NO_FILE:
throw new RteHttpError(400, 'No file received.');
default: // NO_TMP_DIR, CANT_WRITE, EXTENSION: server-side problems
error_log('rte upload: UPLOAD_ERR code ' . $file['error']);
throw new RteHttpError(500, 'The server could not store the upload.');
}
if (!is_uploaded_file($file['tmp_name'])) {
throw new RteHttpError(400, 'No file received.');
}
$size = filesize($file['tmp_name']);
if ($size === false || $size === 0) {
throw new RteHttpError(400, 'The file is empty.');
}
if ($size > RTE_MAX_BYTES) {
throw new RteHttpError(413, 'Images can be at most 4,000,000 bytes.');
}Bei Überschreitung von upload_max_filesize führt PHP das Skript trotzdem aus, und $_FILES['file']['error'] ist UPLOAD_ERR_INI_SIZE. Wir haben eine 3-MB-Datei an einen Server mit dem PHP-Standardwert von 2M gesendet und 413 {"error":"The image is larger than the server accepts."} erhalten.
Bei Überschreitung von post_max_size verwirft PHP den gesamten Body: $_POST und $_FILES sind beide leere Arrays, als wäre nichts gesendet worden. Das Skript läuft trotzdem, und $_SERVER['CONTENT_LENGTH'] enthält weiterhin die tatsächliche Größe. Nur an dieser Kombination lässt sich erkennen, was passiert ist, deshalb prüft das Beispiel sie vor allem anderen. Lassen Sie diese Prüfung weg, sieht ein Upload über dem Limit aus wie „keine Datei empfangen“.
Die Falle mit display_errors
Ist ein Request größer als post_max_size, löst PHP außerdem eine Warnung aus, und zwar vor der ersten Zeile Ihres Skripts. Ist display_errors aktiviert, was der eingebaute Standard von PHP ist, wenn keine php.ini geladen wird, und auch die Einstellung in php.ini-development, wird diese Warnung zum Anfang des Response-Bodys. Ihr http_response_code(413) schlägt dann mit „Cannot modify header information“ fehl, und der Browser erhält Folgendes:
HTTP/1.1 200 OK
Content-type: text/html; charset=UTF-8
<br />
<b>Warning</b>: POST Content-Length of 9000204 bytes exceeds the limit of 8388608 bytes in <b>Unknown</b> on line <b>0</b><br />
...
{"error":"The upload is larger than the server accepts (post_max_size)."}Ein Skript kann das nicht reparieren, weil die Ausgabe gesendet wurde, bevor das Skript startete. Schalten Sie die Anzeige von Fehlern ab und protokollieren Sie sie stattdessen. Unter dem eingebauten Server von PHP half -d display_errors=stderr in unserem Test nicht. Nur -d display_errors=0 ergab ein sauberes HTTP/1.1 413 mit Content-Type: application/json, und die Warnung ging dann ins Server-Log. php.ini-production enthält bereits display_errors = Off.
Der temporäre Ordner und open_basedir
PHP schreibt jeden Upload in eine temporäre Datei, bevor Ihr Skript läuft, in upload_tmp_dir oder den temporären Ordner des Systems. Wenn Sie open_basedir verwenden und diesen Ordner nicht aufnehmen, schlägt jeder Upload fehl, unabhängig von seiner Größe. Das Log zeigt „open_basedir restriction in effect“ und „unable to create a temporary file“, und das Skript sieht UPLOAD_ERR_NO_TMP_DIR, worauf das Beispiel mit einem 500 antwortet. Nachdem wir den temporären Ordner in open_basedir aufgenommen hatten, bestanden alle 35 Vertragsprüfungen. Unter Windows behält -d open_basedir=A;B auf der Kommandozeile stillschweigend nur A, weil ; in einer ini-Datei einen Kommentar einleitet. Setzen Sie den Wert in Anführungszeichen: -d 'open_basedir="A;B"'.
Testen
Starten Sie das Beispielprojekt zu dieser Anleitung mit php -d upload_max_filesize=5M -d display_errors=0 -d log_errors=1 -S localhost:5201 router.php und senden Sie dann eine Datei über dem Limit des Beispiels und eine über post_max_size:
head -c 4500000 /dev/zero > big.jpg
curl -F "file=@big.jpg" http://localhost:5201/rte/upload
{"error":"Images can be at most 4,000,000 bytes."}
head -c 9000000 /dev/zero > huge.jpg
curl -F "file=@huge.jpg" http://localhost:5201/rte/upload
{"error":"The upload is larger than the server accepts (post_max_size)."}Beide Antworten sind HTTP 413 mit einem JSON-Body. Um die Falle zu sehen, starten Sie den Server erneut ohne -d display_errors=0 und senden huge.jpg noch einmal. Bei einem PHP ohne php.ini oder mit php.ini-development kommt die HTML-Warnung vor dem JSON an.
Dieselbe Anleitung für andere Plattformen
JavaScript · Next.js · ASP.NET Core · MVC 5 · Web Forms