ScreenshotNeo

BlogHow-to

How to Validate Images with JavaScript

Validate image uploads in JavaScript with browser checks, previews, and secure server-side enforcement.

By the ScreenshotNeo team1 October 20267 min read

Use JavaScript for fast feedback, but enforce acceptance on the server. In the browser, inspect input.files, allow only the formats your application supports, enforce a size limit, and optionally create a preview with URL.createObjectURL(). Treat those checks as user-interface help. A user can change your JavaScript or send an upload directly, so the server must validate the received bytes independently.

The accept attribute helps guide the file picker, but it is not validation. MDN states that it “doesn’t validate the types of the selected files”; it provides a hint to browsers instead. MDN’s file input reference documents this behavior.

1. Browser validation: complete example

This example accepts PNG and JPEG files up to an application-defined 5 MiB limit. Change both the allowlist and limit to match your product’s policy.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Image validation</title>
  <style>
    #preview { max-width: 480px; max-height: 320px; display: block; margin-top: 1rem; }
  </style>
</head>
<body>
  <form>
    <label for="image">Choose a PNG or JPEG</label>
    <input id="image" type="file" accept="image/png,image/jpeg">
    <p id="message" aria-live="polite"></p>
    <img id="preview" alt="Selected image preview" hidden>
  </form>

  <script>
    const input = document.querySelector("#image");
    const message = document.querySelector("#message");
    const preview = document.querySelector("#preview");
    const allowedTypes = new Set(["image/png", "image/jpeg"]);
    const maxBytes = 5 * 1024 * 1024; // Example policy: 5 MiB
    let previewUrl = null;

    input.addEventListener("change", () => {
      const file = input.files?.[0];

      if (previewUrl) {
        URL.revokeObjectURL(previewUrl);
        previewUrl = null;
      }
      preview.hidden = true;
      preview.removeAttribute("src");

      if (!file) {
        message.textContent = "Choose an image file.";
        return;
      }

      if (!allowedTypes.has(file.type)) {
        message.textContent = "Choose a PNG or JPEG image.";
        input.value = "";
        return;
      }

      if (file.size > maxBytes) {
        message.textContent = "The selected file is too large (maximum 5 MiB).";
        input.value = "";
        return;
      }

      previewUrl = URL.createObjectURL(file);
      preview.src = previewUrl;
      preview.hidden = false;
      message.textContent = `${file.name} is ready to preview.`;
    });
  </script>
</body>
</html>

What each check does

  • input.files returns the selected File objects.
  • file.name is useful for messages, but a filename is user-controlled.
  • file.size is measured in bytes. The 5 MiB value above is an example policy, not a web standard.
  • file.type is the browser-reported MIME type. It can be empty, incorrect, or supplied from user-controlled metadata.
  • URL.createObjectURL(file) lets the browser attempt a preview. A successful preview only shows that this browser could decode the data; it does not prove that the upload is safe.
  • URL.revokeObjectURL() releases the temporary object URL when it is replaced.

2. Format allowlists and the accept attribute

Keep the browser hint and the JavaScript allowlist aligned. For an exact policy, use MIME values such as image/png,image/jpeg or explicit extensions such as .png,.jpg,.jpeg. The broad value image/* may include formats your server cannot decode or safely process.

<input type="file" accept="image/png,image/jpeg">

Users can override a picker’s filter, select a renamed file, or bypass the page entirely. Therefore, accept, the filename extension, and File.type should only produce immediate feedback. They must not decide whether the server stores the file.

3. Preview safely and handle replacement

A preview improves usability, especially when several files look similar. Clear the old preview before processing a new selection and revoke the old object URL. If your interface supports multiple files, iterate over input.files and apply the same checks to every item.

for (const file of input.files ?? []) {
  if (!allowedTypes.has(file.type)) {
    // Show an item-specific error in the UI.
    continue;
  }
  if (file.size > maxBytes) {
    // Reject this item before attempting a preview or upload.
    continue;
  }
  const url = URL.createObjectURL(file);
  // Attach `url` to an img element, then revoke it when that item is removed.
}

Do not use a preview as a security decision. A browser may decode content that your server-side image processor rejects, and a malicious file can be submitted without ever being previewed.

4. Server-side validation is the security boundary

The receiving service must enforce its own allowlist and maximum size on the uploaded bytes. OWASP’s File Upload Cheat Sheet recommends layered controls:

  1. Require only the extensions and image types your application needs.
  2. Set a maximum upload size before processing the file.
  3. Do not trust the submitted Content-Type; OWASP notes that it is user-provided and can be spoofed.
  4. Check file signatures as one signal, never as the only control.
  5. Decode and rewrite accepted images with a maintained image-processing library.
  6. Confirm the detected type is in your allowlist after processing.
  7. Derive the stored extension from detected or rewritten content, not from the original name or header.
  8. Generate a storage filename on the server.
  9. Serve accepted files with the correct response content type.

Signature checks are useful, but OWASP cautions that they should not be used alone because bypassing them is possible. The safest design separates browser convenience checks from server acceptance controls.

A practical request flow

  1. The browser rejects obvious mistakes such as an empty selection, an unsupported declared type, or an oversized file.
  2. The upload endpoint applies an early byte-size limit.
  3. The server detects and decodes the image using an image library.
  4. The server rejects unsupported or undecodable content.
  5. The server rewrites the image into an approved format, assigns a generated name, and stores it outside executable paths.
  6. The serving endpoint returns the correct content type for the processed output.

5. Edge cases to decide explicitly

Case Recommended behavior
No file selected Show a clear message and do not send an upload request.
Multiple files when one is expected Use the first file only or reject the selection; make the rule explicit.
Missing File.type Do not silently accept it. Let the server detect the content.
Renamed extension Ignore the extension for acceptance; inspect and process the bytes on the server.
Very large dimensions Apply pixel or processing limits in the image library to avoid expensive decoding.
Animated or multi-frame images Decide whether to preserve animation or rewrite to a single approved frame.
Client-side cancellation Abort the upload and leave the form usable; never assume a canceled request reached the server.
Network retry Use an idempotency strategy or deduplicate generated uploads so retries do not create unwanted copies.

6. Troubleshooting

“The picker still lets me choose another file type.”

accept is a picker hint, not a validator. Keep the JavaScript feedback check and enforce the real policy on the server.

“A valid image has an empty MIME type.”

Browser metadata is not guaranteed. Do not make an empty file.type an automatic security decision; let the server detect and decode the bytes.

“The preview is blank or throws an error.”

The browser could not decode the selected data, or the object URL was revoked too early. Create the URL after validation, keep it while the image is displayed, and revoke it when replacing or removing the preview.

“The server accepts a renamed executable or document.”

The endpoint is trusting a filename or submitted Content-Type. Enforce a byte-size limit, detect the content, decode and rewrite it with an image processor, and derive the final extension from the processed result.

“Large images exhaust memory or take too long.”

Limit upload bytes and processing dimensions before expensive operations. Reject files that exceed your application’s resource policy and perform rewriting in a controlled worker or service when necessary.

7. Performance, reliability, and cost considerations

  • Reject size and declared-type mistakes in the browser to reduce unnecessary requests, but keep identical server checks.
  • Object URLs avoid converting the entire file to a base64 string, which keeps previews simpler and usually reduces temporary memory overhead.
  • Do not decode or resize an image repeatedly. Process it once on the server and store the approved output.
  • Set request, upload, and image-processing timeouts. Return a specific error when a limit is exceeded.
  • Log rejection reasons without storing sensitive file contents. Track rates for unsupported type, size, decode failure, and processing timeout.
  • Choose limits based on your application’s supported formats, storage, and processing budget. There is no universal JavaScript size limit.

8. Or skip the browser setup

If your workflow needs screenshots of pages or image assets rather than user-upload validation, ScreenshotNeo provides a single GET request that returns PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.

Create a free ScreenshotNeo account.

9. FAQ

Is accept="image/*" enough?

No. It is only a picker hint and may include formats your server does not support.

Can I trust file.type?

Use it for quick feedback, not authorization. It may be missing or spoofed; validate the received bytes on the server.

Does a successful preview prove the upload is safe?

No. Preview decodability is a browser behavior, not a security guarantee.

Should I validate the filename extension?

You may use it to explain an obvious user mistake, but acceptance should be based on server-side detection and processing.

What should the server store?

Store a processed image under an application-generated name and serve it with the correct content type. Do not use the uploaded filename as the storage path.