Software & practice

Process file uploads through explicit acceptance rules

Validate supplied files against a clear contract and control their subsequent use deliberately.

Symbolic upload still life in AZ colours: a rich red plausible SD reader rests on a dark Image inspection folder, with an inserted card and cable in a separate side port. Ivory cards identify JPEG, PNG, type, size and content in German.

Uploading is an acceptance decision

An upload brings someone else’s bytes into the system’s responsibility. It therefore needs an explicit purpose: which files are needed, who may submit them and what happens next? The browser file picker does not answer those questions. A polished drop area can improve operation but replaces neither server acceptance rules nor permission to retrieve and use the stored result afterwards.

Our complete example handles static image material supplied as JPEG or PNG. A PHP module checks the real upload, actual size, recognised type, dimensions and decodability. It then creates a new WebP image under a random identifier in private storage. Fixtures are original artificial images; no actual customer documents or production uploads are processed during this experiment.

The module is exercised through real local HTTP uploads. Seventy-four conditions cover seventeen explicitly verified HTTP requests and additional inspection of stored results. This is not a complete public upload product with authentication and approval management. It is a bounded acceptance core whose behaviour can be understood through concrete accepted files, rejected files and the outputs actually written to disk.

Derive the file contract from the task

Start with the content the next task genuinely requires rather than the longest possible extension allowlist. A profile image, signed agreement and exchange archive have different requirements. This example needs a decodable raster image, so SVG, PDF and GIF are outside its contract even if those formats are appropriate elsewhere. Its boundary follows the stated processing task, not a claim that those formats are universally undesirable.

Limits are two MiB per file, at most 4000 pixels in either direction and no more than eight million pixels in total. These are chosen demonstration limits, not universal recommendations. Combining byte and pixel limits matters because a small compressed file can describe a much larger decoded image. Transfer size alone does not determine later processing cost or memory requirements.

Output is a newly generated static WebP, not an unchanged archive copy. Preserving an original for legal or business reasons requires a different explicitly designed path. Orientation, colour profile and other image requirements likewise belong to the real product decision. This module claims no complete photographic preparation of every JPEG or PNG variant a user could select, despite supporting the specific formats and fixtures shown.

Separate browser hints from server decisions

An accept attribute can suggest appropriate types in a picker. It is an operating aid, not security evidence. Other clients can send requests without using that field, and a declared MIME type can be chosen freely. This receiver therefore examines the file actually received rather than treating the sender’s claim as its authoritative type decision or assuming the ideal form is the only possible client.

The original filename never determines the storage path. A test submits a valid PNG using a path-like PHP filename and an incorrect client MIME type. Content still identifies an acceptable image, which receives a new WebP identifier. This contract deliberately decides by content. A production interface may impose naming conventions too, but should not confuse such a rule with examination of the actual uploaded data.

That does not mean saving arbitrary content under a different extension. HTML labelled JPEG is rejected, as is SVG labelled PNG. Only an allowed recognised raster type, suitable dimensions and successful decoding allow new output. The sender therefore controls neither the eventual filesystem path nor an unchecked original copied directly into the publicly served area. The distinction is exercised with real HTTP requests.

Check the actual upload state

PHP provides an upload error status. The code first requires a suitable single-file structure with an integer error value. An unsuccessful transfer is not processed further. A runtime size rejection differs from a disallowed content type or a later decoding failure. Production feedback should preserve these useful distinctions rather than reporting every outcome as an unexplained generic upload problem.

The is_uploaded_file check establishes that the temporary path belongs to an HTTP upload handled by this PHP flow. A countercheck supplying an ordinary local file is rejected. This addresses the demonstrated confusion between form-upload material and an arbitrarily named existing file. It does not provide authentication, form-action protection or business permission over the object to which an image might subsequently be attached.

An empty file is identified from its real size. A number displayed or supplied by a custom client interface is not accepted as proof. The maximum-size check also uses existing bytes. Requests with no image and a multi-file structure where one file was required are rejected. The input contract therefore remains explicit outside the ideal successful submission made by the intended form.

PHP: is_uploaded_file · Upload Errors

Use type recognition, dimensions and decoding together

Fileinfo identifies the recognised type, and only PNG and JPEG continue. The module then reads dimensions and checks directional and total pixel limits before fully decoding through GD. A surviving header is insufficient. The damaged-PNG fixture retains enough header material for earlier checks but fails actual decoding, demonstrating why identifying an apparent image and obtaining usable raster data are different steps.

None of these checks is described as universal malware detection. The libraries need a maintained runtime and suitable resource limits. Parsing an image is additional processing of untrusted content. More exposed or demanding systems may need stronger isolation. Our checks establish the concrete acceptance cases, not the absence of every unknown parser flaw or the suitability of the same process for arbitrary hostile files.

Pixel limits apply before full decoding. An image 4001 pixels wide and one containing nine million pixels are rejected at that stage. Tests use original fixtures with those real dimensions. This is not a general timing or memory benchmark. Limits are part of a stated contract and need relevant review in the intended environment, including available resources and how many operations can run simultaneously.

PHP: Fileinfo · Image Decoding

Generate private output under a new identity

The code generates a fresh WebP from successfully decoded raster data. It uses neither the original filename nor extension for storage. A random identifier determines the new filename, and the file is created exclusively. Reuploading the same original name produces another identifier instead of overwriting the previous output. Actual tests verify both the separate files and their recognised output type.

The application supplies a trusted private directory outside public delivery. It does not obtain that path from a request field. The local experiment genuinely separates its public receiver from private results, and a direct HTTP request for an apparent stored-image route returns 404. This demonstrates the particular test arrangement rather than assuming any directory named private is automatically protected on every server.

The return value includes only identifier, output format, dimensions and byte count. It grants no retrieval permission and publishes nothing. A real application can associate that identifier with an authorised record. Storing that relationship and implementing protected retrieval are additional responsibilities. An unpredictable filename discourages guessing but should not be presented as complete access control for confidential or restricted material.

Read the complete acceptance core

The following PHP module contains the complete bounded image acceptance logic. It uses Fileinfo, GD and PHP’s upload association. On failure it supplies a clear internal rejection reason or stops output with an exception. The public integration must translate those outcomes into suitable understandable feedback without exposing internal paths or confidential diagnostic details simply because debugging would be convenient.

Failure cleanup concerns only the new result file created by this call. It neither deletes existing images nor cleans a general directory. Successful output remains available for subsequent association. Retention of unassociated files and permission to remove them belong to the real application lifecycle. Those decisions should be explicit rather than assumed to be solved by a temporary-file helper or a random filename.

<?php
declare(strict_types=1);
final class UploadRejected extends RuntimeException {
 public function __construct(public readonly int $status, public readonly string $reason) {
  parent::__construct($reason);
 }
}
function storeImageUpload(array $file, string $privateDirectory): array {
 if (!isset($file['error']) || !is_int($file['error'])) throw new UploadRejected(400,'invalid_upload');
 if ($file['error'] !== UPLOAD_ERR_OK) {
  $tooLarge=in_array($file['error'],[UPLOAD_ERR_INI_SIZE,UPLOAD_ERR_FORM_SIZE],true);
  throw new UploadRejected($tooLarge?413:400,'upload_error');
 }
 $tmp=$file['tmp_name']??null;
 if (!is_string($tmp)||!is_uploaded_file($tmp)) throw new UploadRejected(400,'not_uploaded');
 $bytes=filesize($tmp);
 if ($bytes===false||$bytes<1) throw new UploadRejected(400,'empty_file');
 if ($bytes>2*1024*1024) throw new UploadRejected(413,'file_too_large');
 $mime=(new finfo(FILEINFO_MIME_TYPE))->file($tmp);
 if (!in_array($mime,['image/png','image/jpeg'],true)) throw new UploadRejected(415,'type_not_allowed');
 $size=@getimagesize($tmp);
 if ($size===false||$size[0]<1||$size[1]<1) throw new UploadRejected(422,'invalid_image');
 [$width,$height]=$size;
 if ($width>4000||$height>4000||$width>intdiv(8_000_000,$height)) throw new UploadRejected(413,'image_dimensions');
 $raw=file_get_contents($tmp);
 $image=$raw===false?false:@imagecreatefromstring($raw);
 if ($image===false) throw new UploadRejected(422,'decode_failed');
 $directory=realpath($privateDirectory);
 if ($directory===false||!is_dir($directory)||!is_writable($directory)) {
  imagedestroy($image);throw new RuntimeException('Private Ablage nicht verfügbar.');
 }
 $id=bin2hex(random_bytes(16));
 $path=$directory.DIRECTORY_SEPARATOR.$id.'.webp';
 $handle=@fopen($path,'xb');
 if ($handle===false) {imagedestroy($image);throw new RuntimeException('Ablage konnte nicht angelegt werden.');}
 try {
  imagealphablending($image,false);imagesavealpha($image,true);
  if (!imagewebp($image,$handle,88)||!fflush($handle)) throw new RuntimeException('Bildausgabe fehlgeschlagen.');
 } catch (Throwable $error) {
  fclose($handle);imagedestroy($image);unlink($path);throw $error;
 }
 fclose($handle);imagedestroy($image);
 $storedBytes=filesize($path);
 if ($storedBytes===false||$storedBytes<1) {unlink($path);throw new RuntimeException('Leere Bildausgabe.');}
 // Only metadata is returned. This does not grant a public download URL.
 return ['id'=>$id,'mime'=>'image/webp','width'=>$width,'height'=>$height,'bytes'=>$storedBytes];
}

Regenerated pixels are not an unchanged original

One counterexample appends an explicitly named marker to a valid PNG. The image remains decodable and is regenerated. The marker is absent from the stored WebP. This specifically establishes that the module is not merely moving the original file under another name. Inspection reads the actual output bytes and checks its recognised format and dimensions instead of relying only on returned metadata.

This single result does not guarantee removal of every conceivable dangerous characteristic from every possible image. Misleading visual content, unsuitable subject matter and third-party rights do not disappear through conversion. The module assesses neither usage rights nor editorial suitability. Those business decisions belong to actual review and may require other checks unrelated to whether GD can decode the supplied raster data.

WebP generation uses a fixed quality setting and claims no lossless archival guarantee. Metadata, orientation and colour behaviour need explicit consideration in a real image product. Preserving the original requires a separately designed protected storage path. An upload design should not promise original fidelity and arbitrary transformation simultaneously without explaining what is retained, what changes and which version subsequent workflows actually use.

Coordinate runtime and application limits

A request may be rejected by the web server or PHP before image processing runs. Overall request size, PHP upload limits, temporary storage and the application’s file limit therefore need agreement. The local experiment places PHP’s upload boundary above the two-MiB application rule. This allows a deliberately oversized but genuinely received file to exercise the module’s own rejection instead of failing before it reaches that code.

Multiple files or large additional fields can make a request much larger than the single image. Field checks do not replace a total request limit. Timeouts, memory budgets and concurrency need separate production planning as well. The seventeen verified HTTP requests are functional cases, not a load simulation with many simultaneous users or a measurement of sustained processing capacity under adverse conditions.

Larger processing may sensibly use a queue and separate worker. Acceptance, checking, conversion and publication then become distinct states. An early receipt acknowledgement must not pretend the image is already processed. This bounded example runs synchronously and returns metadata only after output is generated. It claims no existing background worker or operational queue merely because those could later be added around the function.

Add identity, form protection and business permission

The executed local receiver is an isolated test fixture, not a public replacement for application authentication. Before production acceptance, determine who may upload for which object. Later associations need the same authorised relationship. A filename or MIME type carries no such permission. The fact that file content passes an image rule says nothing about whether the sender may attach it to a particular customer or record.

Session-based forms also need appropriate protection against externally triggered actions. Rate limits, storage budgets and an approval step may be required. These are separate from decoding success. An unauthorised person can supply a valid image, while an authorised person can mistakenly choose an invalid file. Keeping those responsibilities distinct makes acceptance failures and access failures understandable and easier to investigate.

The small function remains useful without overstating its scope. Its caller supplies already established application context and a trusted private directory. The function then checks the concrete single-image contract. The complete application connects those layers and accepts its real workflows separately. Passing this unit does not automatically provide security acceptance for a whole public upload-and-publication feature.

Actual eighty-by-fifty-pixel PNG fixture and stored WebP output enlarged for inspection. The appended marker is absent after regeneration. HTML with a JPEG filename and excessive pixels are rejected; private output has no direct HTTP route. Labels are German.
Actual fixture and output bytes from the 74 checks. The image was decoded and regenerated. Removal of the appended marker establishes this specific transformation, not general malware freedom. Explanatory image labels are German.

Review 74 conditions through real HTTP uploads

Execution uses PHP 8.4.27 with Fileinfo and GD. Valid PNG, valid JPEG, a repeated original name and deliberately misleading client naming are transferred through actual HTTP multipart uploads. Results get distinct random identifiers and new WebP files. Stored output is independently examined for format and dimensions. The artificial accepted fixtures are 80 by 50 pixels, making their transformation easy to inspect.

Rejected cases include HTML labelled JPEG, SVG labelled PNG, GIF, excessive bytes, excessive width, excessive pixel count and damaged PNG content. Empty and missing files plus an unexpected multi-file structure are covered too. Rejected cases add no stored results. A direct request to the private result path fails, confirming the concrete public/private separation used in this local fixture arrangement.

The appended-marker case and rejection of an ordinary local file as a supposed upload add further evidence. The explanatory image uses actual returns and stored fixture images. No antivirus product is evaluated and no representative attack-detection or load rate is measured. These 74 conditions establish the displayed core and local storage boundary, leaving wider production integration and operational resilience to their own review.

Make feedback support the next corrective action

After rejection, someone should know what can change. Disallowed image type, excessive size and damaged image are different problems. An interface can give appropriate messages while preserving other correct fields. Internal reasons such as decode_failed help technical evidence but need not appear as confusing product wording. The visitor’s concern is selecting acceptable material and completing the actual task, not interpreting a library condition.

Explain the chosen contract before selection. JPEG or PNG, byte limit and maximum dimensions are relevant for this example. Final server checks remain necessary. Browser-side checks can prevent needless transfer and improve interaction but should express the same rules rather than invent a second conflicting set of limits. Their useful role is guidance, not authority over what the server ultimately accepts.

With multiple stages, feedback should name the state actually reached. A received file may still need inspection or association. Colour alone does not explain these differences reliably. The complete application needs understandable names and reachable correction paths. Technical states from this small module provide a foundation for that interface, not a claim that the full visitor workflow has already been implemented.

Treat retrieval, replacement and deletion as a lifecycle

A stored image belongs to a business object. The application decides again who may view or replace it later. An unpredictable identifier merely discourages guessing and cannot replace permission checking. Private files therefore need a deliberately developed retrieval route. This experiment publishes none of its results and implements no download endpoint; direct retrieval is deliberately outside the acceptance core’s responsibilities.

Replacement should not destroy the old image before successful association of its successor. Conversely, permanently unused files should not accumulate without a retention purpose. Production processing must consider file and database record together. Failures between writing and association need traceable handling that this isolated core does not implement on behalf of an eventual integration or silently claim to make atomic.

Deletion concerns the particular associated content and its actual retention rules. It is not an invitation to clean arbitrary directories using paths supplied by a client. The new identifier and private storage help describe those responsibilities clearly. Only the complete verified lifecycle turns a working image acceptance function into a dependable upload capability serving the real product and its authorised users.

Executed acceptance cases

File or requestOutcome
Valid PNG or JPEG201, new private WebP output
Repeated original nameDifferent identifier, no overwrite
HTML or SVG with image filename415, type disallowed
More than two MiB or excessive pixels413, no result file
Damaged PNG422, decoding failed
Missing or empty file400, no result file
Direct private output request404 in the isolated test arrangement

Controlled-upload checklist

  • Define purpose, accepted content and next actions first.
  • Check identity and permission before acceptance.
  • Do not treat client names or MIME claims as content authority.
  • Check upload errors and genuine temporary-upload association.
  • Bound bytes, dimensions and pixel count.
  • Combine recognised type with actual decoding.
  • Store new output privately under an independent identifier.
  • Inspect actual stored bytes in acceptance evidence.
  • Authorise publication, retrieval, replacement and deletion separately.
  • Review limits, libraries and real integration before release.

Sources and review scope

Primary sources reviewed on 9 November 2026. Original artificial images, 74 executed conditions and 17 explicitly verified HTTP requests using PHP 8.4.27. The isolated receiver is not a public authentication or download workflow. No general malware or capacity guarantee.

Let’s move forward

What would you like to change?

An idea, a specific challenge or your next step with AI. We’re listening.

Let’s talk