Using #upload_validators for safe file handlingfor Drupal 8 , 9 , 10 , and 11

Last updated :  

A file upload field is a door that lets visitors put arbitrary bytes onto your server — even a well-permissioned form becomes a liability if it accepts anything named right. #upload_validators is how you narrow that door: a list of rules Drupal checks before the file is ever saved, rejecting anything that doesn't fit.

What you'll learn in this lesson

  • How to restrict an upload field to specific file extensions and a maximum size
  • Why setPermanent() matters, and what happens if you forget it
  • What genuinely changed for file validation in recent core versions

A validated managed_file field

namespace Drupal\my_module\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\file\Entity\File;

class ImageUploadForm extends FormBase {

  public function getFormId() {
    return 'image_upload_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['image'] = [
      '#type' => 'managed_file',
      '#title' => $this->t('Upload an Image'),
      '#upload_location' => 'public://images/',
      '#upload_validators' => [
        'file_validate_extensions' => ['png jpg jpeg'],
        'file_validate_size' => [2 * 1024 * 1024], // 2MB in bytes.
      ],
    ];

    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Upload'),
    ];

    return $form;
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    $fids = $form_state->getValue('image');
    if ($fids) {
      $file = File::load($fids[0]);
      $file->setPermanent();
      $file->save();
      $this->messenger()->addMessage($this->t('Image uploaded successfully!'));
    }
    else {
      $this->messenger()->addMessage($this->t('Image upload failed. Please try again.'), 'error');
    }
  }
}
Corrected from the original: both drupal_set_message() calls — removed in Drupal 8 — are now $this->messenger()->addMessage(). The array key is also renamed from a generic $file to $fids, since #type => 'managed_file' actually stores an array of file IDs, not a file object — that's exactly why the next line has to File::load() it.

Why the two validators matter separately

file_validate_extensions checks the filename's extension against an allow-list — it does not inspect the file's actual contents, so it's a first line of defense, not a guarantee the file truly is an image. file_validate_size rejects anything over the byte limit before it's fully processed, protecting against both storage abuse and a poorly-behaved upload tying up server resources.

Why setPermanent() is not optional

A freshly-uploaded managed file starts life as temporary — Drupal's file garbage collection will delete any temporary file that isn't attached to something within roughly 6 hours (governed by the file_temporary_maximum_age setting). Skip setPermanent() and your users' uploads will quietly vanish shortly after submission, which reads as a mysterious bug rather than the actually-documented cleanup behavior it is.

What's genuinely different on very recent core: Drupal 10.2 introduced a new FileValidator plugin system (\Drupal\file\Validation\FileValidatorInterface) intended to eventually replace the classic string-keyed #upload_validators array shown above. As of this writing, the classic syntax above still works across Drupal 9, 10, and 11 and remains what you'll see throughout most existing modules — but if you're specifically targeting the newest core versions, be aware the plugin-based system exists as the forward-looking replacement.

Quick check: a user uploads a 50KB file named photo.jpg.php. Does file_validate_extensions => ['png jpg jpeg'] block it? (Answer: yes — the validator checks the file's actual final extension, which here is .php, not just whether "jpg" appears anywhere in the filename. This is exactly the kind of double-extension trick the validator exists to catch.)

Key takeaways

  • #upload_validators runs before a file is accepted — restrict extensions and size here rather than checking after the fact.
  • managed_file stores an array of file IDs in the form value, not a file object — always File::load() before treating it as one.
  • Always call setPermanent() and save() on an uploaded file you intend to keep — temporary files are automatically garbage-collected.
  • Extension validation checks the filename pattern, not file contents — it's a first defense, not a full content-type guarantee.
  • drupal_set_message() is dead Drupal 7 code — use the Messenger service instead.

Coming up next

You've now covered CSRF tokens, input sanitization, permission-based access, and file upload validation — the core security surface of any Drupal form. Next, we close out this section by looking at limiting form access more broadly: combining these techniques to control exactly who can reach a form and under what conditions.