A candlemaker photographs a new product on her kitchen table, drops the file into the media library, and the shop page looks fine. WordPress has quietly turned that one upload into eight or nine files. One of them, the one nobody looks at, is the JPEG her phone wrote, byte for byte, still sitting in wp-content/uploads/2026/08/. Inside it is a latitude and a longitude accurate to a few metres, and they point at her flat.
This is not an obscure edge case. WordPress ships a warning about it in the privacy policy text core suggests for you, in wp-admin/includes/class-wp-privacy-policy-content.php: “If you upload images to the website, you should avoid uploading images with embedded location data (EXIF GPS) included. Visitors to the website can download and extract any location data from images on the website.”
The advice you find elsewhere is usually some version of “don’t worry, WordPress strips it when it resizes”. That is half true on some servers, flatly wrong on others, and never true of the file you actually uploaded. Here is what core really does, checked against the source, and what to do about it.
What is actually inside the file
EXIF is a block of tags the camera writes into the image file itself, in a segment called APP1 that sits right after the two bytes marking the start of a JPEG. It is not a sidecar, not a database record, not something WordPress adds. It travels with the file wherever the file goes.
A typical block holds camera make and model, lens, focal length, aperture, shutter speed, ISO, an orientation flag, the date and time the shutter fired, and the name and version of the last piece of software to save the file. Some cameras add a body serial number. Phones add a GPS sub-block: GPSLatitude, GPSLongitude and their hemisphere references, usually altitude and often a compass bearing. The coordinates are stored as three rational numbers each (degrees, minutes, seconds), and on a modern handset they are commonly good to within a few metres.
IPTC is a separate, older block from the news wire world, stored in a different segment (APP13). It carries the fields an editor cares about: creator, credit line, copyright notice, caption, headline and keywords. XMP is a third block again, an XML document usually written by Adobe software, increasingly used for rights statements and accessibility text.
Three independent blocks, three different jobs. Removing one does not touch the other two, which is why “I ran an optimiser over it” is not an answer to “does it still have coordinates”.
What WordPress reads, and what it keeps
The relevant function is wp_read_image_metadata(), in wp-admin/includes/image.php. It runs twice during a normal upload: once inside media_handle_upload(), to work out a title, caption and alt text for the attachment, and again inside wp_create_image_subsizes(), to build the stored metadata array.
It does not copy the metadata wholesale. It fills a fixed array with exactly these keys: aperture, credit, camera, caption, created_timestamp, copyright, focal_length, iso, shutter_speed, title, orientation, keywords and alt. That array is saved under the image_meta key of the attachment’s _wp_attachment_metadata post meta.
Look at what is missing from that list: latitude and longitude. Core calls PHP’s exif_read_data(), which does return GPSLatitude and GPSLongitude along with everything else, and it hands the whole raw array to the wp_read_image_metadata filter as the fifth argument. But it never stores the coordinates. If you have read that WordPress saves your GPS position to the database, core does not.
What it does store is public. The REST API’s media_details field is wp_get_attachment_metadata() handed back verbatim, and an attachment with the inherit status is readable without logging in. A request to /wp-json/wp/v2/media/123 returns the camera model, the credit line, the copyright notice, the keywords and the capture timestamp of any image on the site, to anyone.
The original file is the one that matters
WordPress never rewrites the file you uploaded. Every derivative is a new file written alongside it. If your upload is wider or taller than 2560 pixels (the default value of the big_image_size_threshold filter), core makes a downscaled copy, appends -scaled to the name, points _wp_attached_file at that copy, and records the untouched original’s filename in $image_meta['original_image']. The mechanics of that split, and of the stack of files a single upload produces, are covered in how WordPress turns one upload into a stack of files.
So the file your visitors load is IMG_4471-scaled.jpg, and IMG_4471.jpg is sitting in the same folder, unmodified and still web accessible. Nobody has to break anything to reach it. The URL is the served one with -scaled removed. It is also handed out directly: wp_prepare_attachment_for_js() puts originalImageURL and originalImageName into the media modal for logged-in editors, and media_details.original_image in the public REST response gives the exact filename to anyone who asks.
The same applies when core converts a format rather than resizing. Since WordPress 6.7, wp_get_image_editor_output_format() maps HEIC and HEIF uploads to JPEG by default, so an iPhone photo shared in its native format arrives, gets converted, and the original HEIC stays on disk as original_image with its GPS sub-block intact.
From there, reading the metadata is not a technique. It is a right-click. Preview on macOS shows it in the inspector, Windows shows a subset in file properties, every photo app has a details pane, and browser extensions read it straight off an image on a page. If the coordinates are in the file, they are available to anyone who wants them.
GD strips it, ImageMagick keeps it
This is the part most write-ups get backwards, and it depends entirely on which image library your host has installed.
With GD, WP_Image_Editor_GD decodes the JPEG into a raw pixel buffer, resamples it, and writes a new file with imagejpeg(). There is no mechanism for carrying metadata across that boundary, so the derivative comes out with no EXIF and no IPTC at all. Nothing is being stripped deliberately. The data simply never makes the trip.
With ImageMagick it is the opposite, and it is deliberate. WP_Image_Editor_Imagick::thumbnail_image() calls strip_meta() on every resize, gated behind the image_strip_meta filter, which defaults to true. But strip_meta() walks the image’s profiles and skips a protected list, and the source comments give the reason for each one:
iccandicm, colour profile information, so colours do not shiftiptc, described in core as copyright dataexif, described as orientation dataxmp, described as rights usage data
The EXIF profile is protected as a whole. There is no way through that code path to keep the orientation flag and drop the GPS sub-block, so on an ImageMagick host the coordinates travel into the -scaled file, into medium, into thumbnail, into everything. WP_Image_Editor_Imagick sits first in the default list of implementations that _wp_image_editor_choose() tries, so it wins wherever it is installed, which on managed WordPress hosting is most of the time. Site Health, under Info and then Media Handling, names the winner in a field called Active editor.
The image_strip_meta filter is no help either way. Return false and strip_meta() never runs, so nothing is removed. Return true and it runs, but the protected list spares the EXIF profile. The filter only decides the fate of the profiles nobody was worried about.
Who this actually bites
The pattern is always the same: someone photographs something at home and publishes the picture, not the address.
- A photographer building a portfolio. Studio shots, test frames and behind-the-scenes images all pin the same set of coordinates, and one of them is where the equipment is kept.
- A small shop owner photographing stock on a kitchen table or a spare room floor. Every product listing carries the same location, so the pattern is obvious even from one photo.
- Estate and letting sites, where interior shots of an empty property go out with the exact position of a building nobody is living in.
- Nurseries, schools and clubs publishing photos taken by parents. The coordinates in those files belong to whoever took the photo, not to the organisation posting it.
The timestamp deserves a mention on its own. created_timestamp is the moment the shutter fired, not the moment you published, and WordPress puts it in the public REST response. Location plus time, repeated across a few dozen images, is a routine.
None of which makes “strip everything” the correct default. A landscape photographer may want coordinates on the file. A studio absolutely wants its copyright notice travelling with the image. The right question is which block, not whether.
Checking what is already in your library
Before changing anything, find out what you are dealing with. For a single attachment, three lines will do. Run this through WP-CLI with wp eval-file, or from a temporary snippet, rather than leaving it in a theme.
$meta = wp_get_attachment_metadata( 42 );
// Everything WordPress kept from the camera and IPTC blocks.
print_r( $meta['image_meta'] );
// The file exactly as it was uploaded, still on disk.
echo wp_get_original_image_path( 42 );
That tells you what is in the database. It does not tell you what is in the file, because core never stored the coordinates. For that you have to read the original directly. The function below is read only: it opens files and reports, and changes nothing.
/**
* Lists attachments whose original file still carries GPS coordinates.
* Read only. Nothing is written or modified.
*/
function wpie_find_located_images( $limit = 500 ) {
$found = array();
if ( ! function_exists( 'exif_read_data' ) ) {
return $found;
}
$ids = get_posts(
array(
'post_type' => 'attachment',
'post_mime_type' => array( 'image/jpeg', 'image/tiff' ),
'numberposts' => $limit,
'fields' => 'ids',
)
);
foreach ( $ids as $id ) {
$path = wp_get_original_image_path( $id );
if ( ! $path || ! file_exists( $path ) ) {
continue;
}
$exif = @exif_read_data( $path );
if ( is_array( $exif ) && ! empty( $exif['GPSLatitude'] ) ) {
$found[ $id ] = $path;
}
}
return $found;
}
On a library of any size this is usually the moment you discover that the answer is “most of the phone photos, none of the stock images”. If you would rather not run code across a few thousand attachments, WunderPaint’s media library manager reads the embedded blocks per image and can remove either just the location tags or every block, without re-encoding the file. Either way it pairs naturally with a general media library cleanup, since the originals nobody is serving are often the same files nobody needed to keep.
You do not have to take anyone’s word for what sits in your own files. Drop a photo below and the viewer reads the EXIF block straight out of the bytes: camera, lens, exposure, timestamps, and the coordinates if they are in there. It runs in this browser tab, the file is never uploaded anywhere.
EXIF viewer
Open a photo and read what it carries: camera, lens, settings, dates, and the spot on the map where the shutter fired. Drop a whole folder of them in and strip the lot in one go, losslessly, by rewriting the container instead of encoding the picture again. Every file is read here in the browser tab and none of them is ever uploaded.
or press Enter to choose them. One to read in detail, or many to clean at once. JPEG, PNG, WebP and TIFF.
Nothing loaded yet.
The picture is drawn from your own disk. Nothing about it is sent anywhere.
Drop a photo above to see its tags.
This photo says where it was taken. Anyone you send the file to can read the same spot, often to within a few metres. Strip it before you post a picture of your home, your child’s school or your workplace.
Nothing here contacts a map service. The link only opens a map if you copy it and paste it into a browser yourself.
Camera and lens, exposure settings, the date and time the shutter fired, the editing software, the author and copyright line, and the GPS position if the camera stored one. Every value is shown next to its raw EXIF tag id, so you can look any of them up.
Each one is cleaned by rewriting the container, not by encoding the picture again, so the compressed image data comes through byte for byte and only the metadata changes. JPEG, PNG and WebP. Nothing is uploaded.
A colour profile says nothing about you, and without it colours shift on wide gamut photos. Keeping the picture upright writes back one single tag and nothing else, because a phone stores the rotation in EXIF rather than in the pixels: take that away with the rest and the photo ends up on its side.
| File | Format | Before | After | Taken out | Note |
|---|
Cleaning is lossless on JPEG, PNG and WebP: the file is walked segment by segment and only the metadata segments are dropped, so every byte of compressed image data survives and the picture is pixel for pixel the one you started with. A TIFF cannot be treated that way, because in a TIFF the metadata structure is the file, and a GIF has no EXIF to begin with. For those two the older route is still there: the picture is drawn onto a canvas and encoded again, which does cost a little quality.
How to remove EXIF data in WordPress on upload
The most reliable place to remove EXIF data is before the file ever reaches WordPress, and I will come back to that. But if uploads arrive from clients or contributors you do not control, you need something that runs on the server.
Hook choice matters more than it looks. The wp_handle_upload filter fires as soon as the file lands in the uploads directory, before WordPress has read anything from it. Strip there and you also destroy the orientation flag, so maybe_exif_rotate() finds nothing to act on and portrait phone photos stay sideways in every generated size. The wp_generate_attachment_metadata filter fires after core has read the metadata, rotated the image and written every derivative, which is exactly what you want.
The helper below removes the EXIF segment from a JPEG by rewriting the file’s segment list. It does not decode or re-encode the pixels, so there is no quality loss, and it leaves the IPTC and XMP blocks in place. It does rewrite the file: once it has run, the EXIF data is not recoverable from that file. Test it on a copy first.
/**
* Removes the Exif block from a JPEG without re-encoding the pixels.
* IPTC (credit, copyright) and XMP are left in place.
*
* This rewrites the file. The Exif data is gone for good afterwards.
*
* @param string $file Absolute path to a JPEG file.
* @return bool True if the file was rewritten, false if it was left alone.
*/
function wpie_strip_jpeg_exif( $file ) {
$data = file_get_contents( $file );
if ( false === $data || strlen( $data ) < 4 || "xFFxD8" !== substr( $data, 0, 2 ) ) {
return false;
}
$out = "xFFxD8";
$pos = 2;
$len = strlen( $data );
$stripped = false;
$complete = false;
while ( $pos + 2 <= $len && "xFF" === $data[ $pos ] ) {
$marker = ord( $data[ $pos + 1 ] );
// Padding byte between segments.
if ( 0xFF === $marker ) {
++$pos;
continue;
}
// Start of scan: the compressed pixels follow, copy the rest untouched.
if ( 0xDA === $marker ) {
$out .= substr( $data, $pos );
$complete = true;
break;
}
// Standalone markers carry no length field.
if ( 0x01 === $marker || ( $marker >= 0xD0 && $marker <= 0xD9 ) ) {
$out .= substr( $data, $pos, 2 );
$pos += 2;
continue;
}
if ( $pos + 4 > $len ) {
return false;
}
$size = unpack( 'n', substr( $data, $pos + 2, 2 ) )[1];
if ( $size < 2 || $pos + 2 + $size > $len ) {
return false;
}
$is_exif = 0xE1 === $marker && "Exifx00x00" === substr( $data, $pos + 4, 6 );
if ( $is_exif ) {
$stripped = true;
} else {
$out .= substr( $data, $pos, $size + 2 );
}
$pos += $size + 2;
}
if ( ! $complete || ! $stripped ) {
return false;
}
return false !== file_put_contents( $file, $out );
}
Only the APP1 segment that starts with the Exif marker is dropped. XMP also lives in an APP1 segment, and it is left alone because its payload starts with an Adobe namespace URI instead. Then wire the helper to new uploads. It cleans the sub-sizes as well, because on an ImageMagick host those carry the same block.
add_filter(
'wp_generate_attachment_metadata',
function ( $metadata, $attachment_id, $context ) {
if ( 'create' !== $context || 'image/jpeg' !== get_post_mime_type( $attachment_id ) ) {
return $metadata;
}
$attached = get_attached_file( $attachment_id );
if ( ! $attached ) {
return $metadata;
}
// The uploaded original, plus the file WordPress now serves as "full".
$originals = array_unique(
array_filter( array( wp_get_original_image_path( $attachment_id ), $attached ) )
);
foreach ( $originals as $path ) {
if ( file_exists( $path ) ) {
wpie_strip_jpeg_exif( $path );
}
}
// On ImageMagick hosts the sub-sizes carry the same block.
if ( ! empty( $metadata['sizes'] ) ) {
$dir = trailingslashit( dirname( $attached ) );
foreach ( $metadata['sizes'] as $name => $size ) {
if ( empty( $size['file'] ) ) {
continue;
}
$path = $dir . $size['file'];
if ( file_exists( $path ) && wpie_strip_jpeg_exif( $path ) ) {
clearstatcache( true, $path );
$metadata['sizes'][ $name ]['filesize'] = wp_filesize( $path );
}
}
}
if ( file_exists( $attached ) ) {
clearstatcache( true, $attached );
$metadata['filesize'] = wp_filesize( $attached );
}
return $metadata;
},
10,
3
);
This affects new uploads only. Existing files stay as they are until you run the stripper over them yourself, which you should do deliberately and with a backup, not as a side effect of loading a page. Both snippets belong in a small site-specific plugin or a child theme’s functions file rather than in a parent theme that will be updated over you. Where custom code belongs and how to edit it safely covers the trade-offs.
Doing it before the upload instead
Server-side stripping is a safety net. It is not the first line, because it only ever runs on files that have already left the photographer’s machine and been sent over the wire.
On the phone itself, both platforms let you drop location when sharing. On recent versions of iOS the share sheet has an options row where location can be switched off before sending. On Android, the camera app has a location tagging switch, and turning it off means the coordinates are never written in the first place. Desktop editors handle this in the export dialog: Lightroom’s export panel has a metadata dropdown with options that remove location while keeping copyright, and Photoshop’s Export As writes a much thinner block than Save As does. The exact wording moves between versions, so look for the word location rather than a specific menu path.
For a folder of files already on disk, exiftool is the precise tool, and it can remove one block without disturbing the rest.
# Remove only the location tags, keep camera settings and credit.
exiftool -gps:all= -overwrite_original photo.jpg
# Remove every metadata block from every JPEG in a folder.
exiftool -all= -overwrite_original -ext jpg .
The second form takes the colour profile with it, which can shift the appearance of wide-gamut images; adding --icc_profile:all to that command keeps the profile. ImageMagick’s own magick input.jpg -strip output.jpg does the same broad removal and carries the same caveat, plus it re-encodes the image, so you pay a quality cost that exiftool does not charge you.
One thing worth knowing about image CDNs: services that resize on delivery generally serve clean derivatives, because they re-encode. That does not clean your origin. The file on your own server keeps whatever it had, and it is usually still reachable.
The metadata worth keeping
Blanket removal throws away the fields that were designed to protect you. The IPTC block is where a headline (2#105), a byline (2#080), a credit line (2#110), a copyright notice (2#116), a caption (2#120) and keywords (2#025) live, and wp_read_image_metadata() reads all of them by those exact record numbers.
It does not just read them, it uses them. During media_handle_upload(), an IPTC headline or a short caption becomes the attachment’s title, the caption becomes the attachment’s excerpt, which is the Caption field in the media library, and the copyright and credit end up in image_meta where a theme can print them under the image. Set those fields once in an export preset and every upload arrives already attributed, with no typing in the media library.
WordPress 7.0 added a genuinely useful piece to this. wp_get_image_alttext() parses the XMP block for the Iptc4xmpCore:AltTextAccessibility property and, if it finds one, uses it as the attachment’s alt text, picking the entry that best matches the site locale. Alt text written once in the photo editor now follows the file into every site it is used on, which beats remembering to fill the field in the media modal. It does not save you from thinking about the text itself, which is a separate craft covered in writing alt text people can actually use.
So the useful policy is not “strip everything”. It is: kill the GPS sub-block, keep the IPTC and XMP blocks, and decide about the camera settings based on whether anyone benefits from reading them.
Symptoms and causes
The thumbnail has no metadata but the full-size URL still shows coordinates. Your server is using GD, which rebuilds derivatives from raw pixels and cannot carry metadata across. The file you uploaded was never rewritten, so it kept everything.
Every size carries the location, thumbnails included. Your server is using ImageMagick. WP_Image_Editor_Imagick::strip_meta() protects the exif, iptc and xmp profiles by design, so they are copied into each generated file.
You ran a metadata-removal plugin and the original is still dirty. Most image optimisers work on the sizes WordPress serves. The file named in image_meta['original_image'] is not served by any template, so it is frequently skipped, and it is the file with everything in it.
Photos started uploading sideways after you added a stripping filter. The filter ran before maybe_exif_rotate() could read the orientation tag. Move it to wp_generate_attachment_metadata, which fires after rotation and after every sub-size has been written.
An attachment’s title or caption filled itself in with something you did not type. That is wp_read_image_metadata() doing its job, pulling an IPTC headline or caption out of the file during upload. Harmless, and often helpful once you know where it comes from.
Where that leaves you
The mental model that keeps this straight is that WordPress treats your upload as an archive copy. It reads a few fields out of it, generates a family of derivatives from it, and then leaves it alone forever. Everything above follows from that one design decision. The derivatives may or may not be clean depending on your host’s image library, but the archive copy is always exactly what you sent, and it is always reachable.
The practical version is short. Check what your library is holding before you change anything. Turn off location tagging at the camera, because a coordinate that was never written cannot leak. Add a server-side strip for uploads you do not control, hooked late enough that WordPress has already used the orientation flag and the IPTC fields. Then leave the credit and copyright blocks alone, because those are the ones working for you.
Most sites will find that a few dozen files need attention and the rest never had anything in them. That is a good outcome, and it is worth twenty minutes to know which group you are in rather than assuming.