Your Photos Are Leaking Precise Location Data — And Most Developers Don’t Handle It
A technical deep-dive into EXIF GPS metadata, how it works, what it exposes, and how to strip it properly in your application
Every time a user uploads a photo to your application, there is a good chance you are storing their home address.
Not intentionally. Not maliciously. Just by accepting a file and saving it without stripping the hidden metadata layer that every modern smartphone embeds by default.
This is not a theoretical vulnerability. It is a default behaviour of every iOS and Android device with location services enabled, which is most of them, most of the time.
Let me show you exactly what is happening, what data is exposed, and how to fix it properly in your stack.
What EXIF data actually contains
EXIF (Exchangeable Image File Format) is a standard defined in 1995 that specifies how metadata is stored inside image files. When your iPhone or Android device captures a photo, it writes dozens of data fields into the file alongside the pixel data.
Here is a real EXIF dump from an iPhone 15 photo, parsed with ExifTool:
ExifIFD:
Make : Apple
Model : iPhone 15 Pro
Software : 18.3.1
DateTime : 2026:03:15 09:42:11GPS:
GPSLatitudeRef : N
GPSLatitude : 48 deg 51' 30.12" N
GPSLongitudeRef : E
GPSLongitude : 2 deg 17' 40.44" E
GPSAltitude : 42.3 m Above Sea Level
GPSTimeStamp : 08:42:11
GPSSpeedRef : K
GPSSpeed : 0
GPSImgDirectionRef : T
GPSImgDirection : 273.5
GPSDestBearingRef : T
GPSDestBearing : 273.5
GPSHPositalError : 4.872Composite:
GPSPosition : 48 deg 51' 30.12" N, 2 deg 17' 40.44" E
That GPS position maps to a specific street in Paris — accurate to within 5 meters.
If this were a photo taken at someone’s home, you now have their home address. If it was taken at a domestic violence shelter, a rehab facility, or a protest, the consequences of leaking this data range from embarrassing to genuinely dangerous.
The full data exposure surface
GPS coordinates are the obvious concern, but EXIF exposes significantly more:
Device fingerprinting:
Make + Model + Software → identifies the exact device
LensModel → further narrows device identification
Behavioral patterns:
DateTime → when the photo was taken
GPSSpeed → whether the user was moving
GPSImgDirection → which direction they were facing
Technical fingerprinting:
ColorSpace → camera sensor characteristics
MeteringMode → shooting behavior patterns
Editing history (XMP):
HistorySoftwareAgent → which apps edited the photo
HistoryChanged → edit timestamps
DerivedFrom → original file referenceCross-reference GPS coordinates from multiple photos over time, and you can reconstruct a user’s daily routine — home location, workplace, gym, and regular restaurants. This is not hypothetical. This is basic OSINT.
How most applications handle this (incorrectly)
The most common handling I see in codebases:
// Express + Multer — the naive approach
app.post('/upload', upload.single('photo'), async (req, res) => {
const filename = `${Date.now()}-${req.file.originalname}`
await fs.writeFile(`uploads/${filename}`, req.file.buffer)
await db.photos.create({ filename, userId: req.user.id })
res.json({ success: true, filename })
})This saves the file exactly as received — EXIF data intact, GPS coordinates stored permanently, available to anyone who can access the file.
Get Achraf Sabbar’s stories in your inbox
Join Medium for free to get updates from this writer.
The same pattern appears in:
- S3 uploads that preserve the original file
- CDN-served images with public URLs
- Gallery applications that display original files
- Profile photo uploads across thousands of SaaS applications
How major platforms actually handle it
For reference, here is what the major platforms do:
Platform GPS stripped Other EXIF Notes
────────────────────────────────────────────────────
Instagram Yes Partial Compression strips most
Facebook Yes Partial Server-side processing
Twitter/X Yes Most Image re-encoded
WhatsApp Partial Partial Compression mode strips GPS,
document mode preserves all
Telegram Partial Partial Same as WhatsApp
iMessage No No Full EXIF preserved
Email No No Full EXIF preserved
Dropbox No No Full EXIF preservedThe platforms that strip metadata do so as a deliberate privacy measure combined with re-encoding for compression. Most developer-built applications do neither.
Stripping EXIF correctly — implementation by stack
Node.js — Sharp (recommended)
Sharp is the standard Node.js image processing library. It strips all metadata by default when you process an image:
import sharp from 'sharp'async function stripMetadata(inputBuffer) {
return sharp(inputBuffer)
.withMetadata(false) // explicitly strip all EXIF/XMP/IPTC
.toBuffer()
}// In your upload handler:
app.post('/upload', upload.single('photo'), async (req, res) => {
const cleanBuffer = await stripMetadata(req.file.buffer)
const filename = `${Date.now()}.jpg`
await s3.putObject({
Bucket: process.env.S3_BUCKET,
Key: filename,
Body: cleanBuffer,
ContentType: 'image/jpeg',
})
res.json({ success: true })
})
If you need to preserve specific fields (copyright, for example) while stripping GPS:
async function stripGPSOnly(inputBuffer) {
// Read existing metadata
const metadata = await sharp(inputBuffer).metadata()
return sharp(inputBuffer)
.withMetadata({
exif: {
IFD0: {
Copyright: metadata.exif?.IFD0?.Copyright,
Artist: metadata.exif?.IFD0?.Artist,
}
// GPS IFD intentionally omitted
}
})
.toBuffer()
}Python — Pillow
from PIL import Image
import iodef strip_metadata(image_bytes: bytes) -> bytes:
img = Image.open(io.BytesIO(image_bytes))
# Create new image without metadata
clean = Image.new(img.mode, img.size)
clean.putdata(list(img.getdata()))
output = io.BytesIO()
clean.save(output, format=img.format or 'JPEG', quality=95)
return output.getvalue()For more surgical GPS-only removal:
from PIL import Image
import piexifdef strip_gps_only(image_bytes: bytes) -> bytes:
img = Image.open(io.BytesIO(image_bytes))
if 'exif' not in img.info:
return image_bytes
exif_dict = piexif.load(img.info['exif'])
# Remove GPS IFD entirely
exif_dict.pop('GPS', None)
exif_bytes = piexif.dump(exif_dict)
output = io.BytesIO()
img.save(output, format='JPEG', exif=exif_bytes, quality=95)
return output.getvalue()Go — goexif + imaging
package mainimport (
"bytes"
"image/jpeg"
"golang.org/x/image/draw"
"image"
)func stripMetadata(input []byte) ([]byte, error) {
img, _, err := image.Decode(bytes.NewReader(input))
if err != nil {
return nil, err
}
// Re-encode without metadata
var output bytes.Buffer
err = jpeg.Encode(&output, img, &jpeg.Options{Quality: 95})
if err != nil {
return nil, err
}
return output.Bytes(), nil
}
Next.js API route
// app/api/upload/route.ts
import sharp from 'sharp'
import { NextRequest, NextResponse } from 'next/server'export async function POST(req: NextRequest) {
const formData = await req.formData()
const file = formData.get('photo') as File
if (!file) {
return NextResponse.json({ error: 'No file' }, { status: 400 })
}
const buffer = Buffer.from(await file.arrayBuffer())
// Strip all metadata before processing
const cleanBuffer = await sharp(buffer)
.withMetadata(false)
.jpeg({ quality: 90 })
.toBuffer()
// Now safe to store
// await uploadToS3(cleanBuffer)
return NextResponse.json({ success: true })
}What about client-side stripping?
You might consider stripping metadata client-side using the Canvas API before uploading:
async function stripMetadataClientSide(file) {
return new Promise((resolve) => {
const img = new Image()
const url = URL.createObjectURL(file)
img.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = img.naturalWidth
canvas.height = img.naturalHeight
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
canvas.toBlob((blob) => {
URL.revokeObjectURL(url)
resolve(blob)
}, file.type, 0.92)
}
img.src = url
})
}This works and is better than nothing — but do not rely on it exclusively. Client-side code can be bypassed. Always strip server-side as well. Defense in depth.
The audit: check your existing uploads
If you have an existing application with a media library, you probably have EXIF data stored in files you uploaded months or years ago. Here is how to audit and remediate:
# Install ExifTool
brew install exiftool # macOS
apt install libimage-exiftool-perl # Ubuntu# Check a single file
exiftool your-image.jpg | grep -i gps# Audit an entire directory
exiftool -csv -GPSLatitude -GPSLongitude ./uploads/ | grep -v "^File"# Strip GPS from all files in a directory (in-place)
exiftool -GPS:all= -overwrite_original ./uploads/# Strip ALL metadata from all files
exiftool -all= -overwrite_original ./uploads/
For production systems with files stored in S3:
import { S3Client, GetObjectCommand, PutObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3'
import sharp from 'sharp'const s3 = new S3Client({ region: process.env.AWS_REGION })async function remediateS3Bucket(bucket, prefix = '') {
const list = await s3.send(new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix }))
for (const object of list.Contents || []) {
if (!object.Key.match(/\.(jpg|jpeg|png|webp)$/i)) continue
// Download
const response = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: object.Key }))
const buffer = Buffer.from(await response.Body.transformToByteArray())
// Strip metadata
const cleanBuffer = await sharp(buffer).withMetadata(false).toBuffer()
// Re-upload
await s3.send(new PutObjectCommand({
Bucket: bucket,
Key: object.Key,
Body: cleanBuffer,
ContentType: response.ContentType,
}))
console.log(`Cleaned: ${object.Key}`)
}
}
Privacy by design — the right architecture
Rather than treating metadata stripping as an afterthought, build it into your upload pipeline:
User uploads photo
↓
Validate file type and size
↓
Extract any needed metadata (if your app uses it)
↓
Strip ALL metadata ← this is the step most apps skip
↓
Re-encode at target quality
↓
Generate thumbnail
↓
Store clean file
↓
Return URL to clientIf your application genuinely needs location data — a travel photo app, a geotagging service — ask for it explicitly through a form field or the Geolocation API rather than silently reading it from EXIF. This is both better UX and more legally defensible under GDPR and CCPA.
Legal exposure
This is worth noting briefly: under GDPR, GPS coordinates embedded in a photo are personal data. Storing them without a legal basis and without disclosing it in your privacy policy is a compliance issue, not just a privacy concern.
Most developer-built applications that store user photos have never considered this. The fix is simple — strip the data — but the legal obligation exists whether you have considered it or not.
Tools for testing your implementation
After implementing metadata stripping, verify it is working:
# Test a file your application processed
exiftool processed-image.jpg# Should return no GPS fields
# Should return minimal or no EXIFOr use a free online metadata viewer like ProMetadata to upload a test file and verify the output has no GPS coordinates.
Summary
Every photo uploaded to your application from a smartphone likely contains GPS coordinates accurate to within 5 meters. The fix is a single function call in Sharp, Pillow, or any image processing library.
The implementation takes 15 minutes. The liability and privacy implications of not implementing it accumulate with every photo your users upload.
// The entire fix, distilled:
const clean = await sharp(buffer).withMetadata(false).toBuffer()Strip metadata server-side on every upload. Audit your existing media library. Treat location data as personal data because, under most privacy laws, it is.
Your users are trusting you with their files. Handle them accordingly.
If you want to check what metadata is hiding in your own files, ProMetadata provides a free online metadata viewer — no login, no software, instant results. You can also use it to strip or inject metadata in bulk for SEO purposes.
Tags: Privacy, JavaScript, Node.js, Web Development, Security, EXIF, Image Processing, GDPR
Canonical URL: https://www.prometadata.com/blog/exif-data-privacy-developer-guide








