Every JPEG your phone or camera produces ships with a hidden passenger: EXIF metadata. Most people never look at it. Most developers never strip it. And that gap has leaked everything from celebrities' home addresses to the exact location of an active-duty soldier's base.
If you build anything that touches user-uploaded images — a marketplace, a social app, a photo gallery — this is one of those quiet responsibilities that's easy to ignore until it's a headline. Here's what's actually in that metadata, how to read it, and how to strip it in a few lines of code.
What's hiding in a photo
EXIF (Exchangeable Image File Format) is a block of key–value data embedded in the file itself. A typical photo carries:
- GPS coordinates — latitude, longitude, sometimes altitude. Precise enough to drop a pin on your front door.
- Timestamps — when the shot was taken, down to the second.
- Camera & lens — make, model, and often the camera body's serial number.
- Settings — ISO, aperture, shutter speed, focal length.
- Software — what edited the file and when.
Individually, harmless. Together, they're a movement log. Geotagged photos posted over time draw a map of where you live, work, sleep, and travel — and the timestamps tell anyone when you're not home.
Reading EXIF (so you can see the problem)
Python (Pillow)
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
img = Image.open("photo.jpg")
exif = img.getexif()
for tag_id, value in exif.items():
tag = TAGS.get(tag_id, tag_id)
print(f"{tag:25}: {value}")
# GPS lives in its own sub-block (IFD)
gps_ifd = exif.get_ifd(0x8825) # GPSInfo
for key, val in gps_ifd.items():
print(f"{GPSTAGS.get(key, key):25}: {val}")
Turning the raw GPS tuples into decimal degrees:
def to_decimal(dms, ref):
degrees, minutes, seconds = [float(x) for x in dms]
dec = degrees + minutes / 60 + seconds / 3600
return -dec if ref in ("S", "W") else dec
lat = to_decimal(gps_ifd[2], gps_ifd[1]) # GPSLatitude, GPSLatitudeRef
lon = to_decimal(gps_ifd[4], gps_ifd[3]) # GPSLongitude, GPSLongitudeRef
print(f"https://maps.google.com/?q={lat},{lon}")
Run that on a geotagged photo and you'll get a clickable map link straight to wherever it was taken. That's the whole point — it's that easy for anyone else, too.
JavaScript (browser, with exifr)
import exifr from 'exifr';
// Read just the GPS — works on a File from an <input>
const gps = await exifr.gps(file);
console.log(gps?.latitude, gps?.longitude);
// Or pull everything
const data = await exifr.parse(file);
console.log(data.Make, data.Model, data.DateTimeOriginal);
exiftool photo.jpg # dump everything
exiftool -gps:all photo.jpg # just the location data
Stripping EXIF (so you stop being the problem)
One-off, CLI
# ExifTool — removes all metadata
exiftool -all= photo.jpg
# ImageMagick — re-encode without metadata
convert photo.jpg -strip clean.jpg
Python
# piexif: surgical removal, keeps the pixels untouched
import piexif
piexif.remove("photo.jpg", "clean.jpg")
# Pillow: re-save without the exif kwarg
from PIL import Image
img = Image.open("photo.jpg")
img.save("clean.jpg") # no exif= passed → metadata dropped
Server-side on upload (Node, sharp)
This is the one that matters in production. Strip metadata as part of your upload pipeline, automatically, for every image. Don't trust users to do it.
import sharp from 'sharp';
await sharp(inputBuffer)
.rotate() // bake in EXIF orientation BEFORE you drop it
.jpeg({ quality: 82 })
.toFile('clean.jpg'); // sharp strips metadata by default
⚠️ Gotcha: EXIF has an Orientation tag that tells viewers to rotate the image. If you strip metadata without applying that rotation first, photos that looked fine suddenly appear sideways. Always auto-orient (sharp's .rotate() with no args) before discarding metadata.
Client-side, before it ever uploads
Drawing an image onto a <canvas> and re-exporting it discards EXIF as a side effect:
function stripExif(file) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
canvas.getContext('2d').drawImage(img, 0, 0);
canvas.toBlob(resolve, 'image/jpeg', 0.9);
};
img.src = URL.createObjectURL(file);
});
}
(Same orientation caveat applies — handle rotation before you rely on this.)
Sometimes. Most large social networks strip EXIF on upload — but the behavior is inconsistent and changes without notice. Direct file delivery (email attachments, cloud links, raw downloads, messaging apps that send "as file") frequently keeps everything intact. The safe assumption: if you didn't strip it, it's still there.
There's a real trade-off, too. Photographers and archivists often want metadata — copyright, camera settings, capture time. So the right rule isn't "always nuke it," it's be deliberate: strip location and serial numbers from anything public-facing, and keep the rest only where it's genuinely useful and the user consents.
Where this shows up in the real world
When you run an image-heavy product, metadata handling stops being a curiosity and becomes part of your duty of care. At Fotio, where the whole business is delivering professional photo galleries from shoots across Spain, this is exactly the kind of detail that separates a polished pipeline from a leaky one — high-resolution images that look great, without quietly broadcasting a client's GPS trail. If you're curious how an image-first product is put together end to end, you can poke around at fotio.app.
The one-line takeaway
Treat EXIF like any other untrusted, potentially sensitive payload: inspect it, strip what's risky, and never assume someone else already handled it.
Add a metadata-stripping step to your upload pipeline today. It's a handful of lines, and it closes a privacy hole most apps don't even know they have.
Do you strip EXIF in your projects — client-side, server-side, or not at all? And have you ever found something alarming in a photo's metadata? Share it in the comments.