A user uploads a photo to your application. The image looks harmless, but the JPEG may contain much more than pixels: GPS coordinates, camera model, capture time, orientation data, editing software, and other EXIF metadata.
For a media-heavy SaaS, that can become a privacy problem surprisingly quickly.
You could strip metadata after the file reaches your backend, but there is another useful option: sanitize the image in the browser before the upload begins.
This is especially useful when the original metadata has no business value.
What Are We Actually Trying to Remove?
A typical JPEG can contain metadata such as:
GPS latitude and longitude
camera manufacturer and model
capture date
lens information
image orientation
editing software
copyright fields
embedded thumbnails
The simplest way to remove most of this metadata is not to parse every EXIF block individually.
Instead, decode the image into pixels and encode a completely new image.
The browser gives us the primitives to do that.
The basic pipeline looks like this:
Original file
↓
Decode image
↓
Draw pixels to canvas
↓
Encode new JPEG/WebP
↓
Upload sanitized Blob
The important detail is that the canvas contains pixel data, not the original JPEG metadata.
When we export the canvas, the browser creates a new image file.
A Minimal Implementation
Here is a small utility that accepts an uploaded image and returns a new JPEG blob:
async function sanitizeImage(file, quality = 0.9) {
const bitmap = await createImageBitmap(file);
const canvas = document.createElement("canvas");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Canvas 2D context is unavailable");
}
ctx.drawImage(bitmap, 0, 0);
bitmap.close();
return new Promise((resolve, reject) => {
canvas.toBlob(
blob => {
if (!blob) {
reject(new Error("Image encoding failed"));
return;
}
resolve(blob);
},
"image/jpeg",
quality
);
});
}
Usage:
const input = document.querySelector("#photo");
input.addEventListener("change", async event => {
const file = event.target.files?.[0];
if (!file) return;
const sanitized = await sanitizeImage(file);
console.log("Original:", file.size);
console.log("Sanitized:", sanitized.size);
await uploadFile(sanitized);
});
The resulting JPEG is created from decoded pixels instead of copying the original file byte-for-byte.
That distinction is what removes the original EXIF payload.
Keep the Filename and MIME Type Separate
A Blob does not have a filename. If your upload API expects a normal file object, wrap it in a File:
function blobToFile(blob, originalName) {
const baseName = originalName.replace(/.[^.]+$/, "");
return new File(
[blob],
${baseName}.jpg,
{
type: "image/jpeg",
lastModified: Date.now()
}
);
}
Then:
const cleanBlob = await sanitizeImage(originalFile);
const cleanFile = blobToFile(cleanBlob, originalFile.name);
await uploadFile(cleanFile);
Do not trust the original file extension to describe the newly encoded content.
If you converted a PNG or WebP to JPEG, update the filename accordingly.
Resizing at the Same Time
Once the image is being decoded anyway, this is also a convenient place to enforce upload dimensions.
A 9000×6000 photo does not need to become a 9000×6000 preview.
function fitInside(width, height, maxWidth, maxHeight) {
const scale = Math.min(
maxWidth / width,
maxHeight / height,
1
);
return {
width: Math.round(width * scale),
height: Math.round(height * scale)
};
}
Use it before drawing:
const size = fitInside(
bitmap.width,
bitmap.height,
3000,
3000
);
canvas.width = size.width;
canvas.height = size.height;
ctx.drawImage(
bitmap,
0,
0,
size.width,
size.height
);
Now one browser-side operation can both remove metadata and reduce unnecessarily large uploads.
That can significantly reduce network traffic for image-heavy applications.
Do Not Block the UI With Huge Images
Canvas operations are CPU- and memory-intensive.
A 50-megapixel image can require hundreds of megabytes of temporary memory once decoded.
Processing several images simultaneously is an easy way to freeze a browser tab.
Avoid this:
await Promise.all(
files.map(file => sanitizeImage(file))
);
For large uploads, use a small concurrency limit.
for (const file of files) {
const sanitized = await sanitizeImage(file);
await uploadFile(sanitized);
}
Sequential processing is slower, but much safer on mobile devices.
A more advanced implementation can process two or three images concurrently or move image work into workers where browser support permits.
Canvas Sanitization Is Not a Security Boundary
This technique is useful, but it should not replace server-side validation.
The backend should still verify:
MIME type
actual file signature
file size
image dimensions
authentication
authorization
storage path
upload ownership
Never assume that a browser-generated request came from your frontend code.
An attacker can bypass your JavaScript entirely and call the upload API directly.
Client-side sanitization improves privacy and efficiency. Server-side validation protects the system.
You normally want both.
Watch Out for Image Orientation
Older JPEG workflows often rely on EXIF orientation tags.
If metadata is removed incorrectly, portrait photos can suddenly appear rotated.
Modern browser decoding generally handles orientation before rendering, but this area has historically differed between APIs and browser versions.
Test uploads from actual phones, especially:
iPhones
Android devices
DSLR and mirrorless cameras
exported Lightroom files
Do not validate the pipeline using only images created on your development machine.
What About Original Downloads?
Sometimes removing metadata is exactly what you do not want.
A photography application might need two representations:
original.jpg
├── original download
└── sanitized web preview
That architecture lets you preserve the photographer's original file while exposing a metadata-free derivative to viewers.
It also avoids repeatedly transforming the original.
For systems with customer-facing galleries, this separation is usually cleaner than treating one file as both archival media and web delivery media.
Final Architecture
A practical upload flow can look like this:
User selects image
↓
Validate basic file type
↓
Decode image in browser
↓
Resize if necessary
↓
Re-encode without original metadata
↓
Upload sanitized derivative
↓
Server performs independent validation
↓
Store and deliver through media pipeline
Metadata privacy is easy to overlook because nothing looks wrong visually.
But location data and camera information can survive every UI redesign while quietly traveling with the file.
Treat metadata as part of your data model, not as an invisible property of an image.
Top comments (0)