{"id":336996,"date":"2026-07-11T12:02:17","date_gmt":"2026-07-11T12:02:17","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/auto-upload-external-images\/"},"modified":"2026-07-11T12:02:07","modified_gmt":"2026-07-11T12:02:07","slug":"nerdcow-external-image-importer","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/nerdcow-external-image-importer\/","author":14040898,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.0.1","stable_tag":"1.0.1","tested":"7.0.1","requires":"6.0","requires_php":"7.4","requires_plugins":null,"header_name":"External Image Importer - Import Remote Images to the Media Library","header_author":"NerdCow","header_description":"Scans post content for externally-hosted (\"foreign\") images, securely downloads them into the local Media Library, and rewrites the URLs to the local copies. Built SSRF-safe by design. Runs automatically on save; WP-CLI command for retroactive fixes.","assets_banners_color":"ff96b2","last_updated":"2026-07-11 12:02:07","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"","header_author_uri":"https:\/\/nerdcow.co.uk","rating":0,"author_block_rating":0,"active_installs":0,"downloads":26,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.0.1":{"tag":"1.0.1","author":"NerdCow","date":"2026-07-11 12:02:07"}},"upgrade_notice":{"1.0.1":"<p>Skips re-uploading images that already exist in your Media Library (matched by exact filename), avoiding duplicates when migrating content.<\/p>","1.0.0":"<p>Initial release of External Image Importer.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3603890,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3603890,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3603890,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3603890,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.0.1"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[15235,11822,233,4155,25065],"plugin_category":[59],"plugin_contributors":[86264],"plugin_business_model":[],"class_list":["post-336996","plugin","type-plugin","status-publish","hentry","plugin_tags-external-images","plugin_tags-import-images","plugin_tags-media-library","plugin_tags-migration","plugin_tags-remote-images","plugin_category-utilities-and-tools","plugin_contributors-nerdcow","plugin_committers-nerdcow"],"banners":{"banner":"https:\/\/ps.w.org\/nerdcow-external-image-importer\/assets\/banner-772x250.png?rev=3603890","banner_2x":"https:\/\/ps.w.org\/nerdcow-external-image-importer\/assets\/banner-1544x500.png?rev=3603890","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/nerdcow-external-image-importer\/assets\/icon-128x128.png?rev=3603890","icon_2x":"https:\/\/ps.w.org\/nerdcow-external-image-importer\/assets\/icon-256x256.png?rev=3603890","generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p><strong>External Image Importer<\/strong> automatically uploads externally-hosted images to your Media Library when you paste external content into the WordPress editor, then rewrites every URL to point at your own local copy. It is built <strong>SSRF-safe<\/strong>: every remote fetch is validated and hardened before the file is downloaded.<\/p>\n\n<p>Remove the hassle of importing external images by hand when you:<\/p>\n\n<ul>\n<li>migrate to WordPress from another CMS;<\/li>\n<li>work across multiple environments, such as development, staging and production;<\/li>\n<li>paste external content drafts written elsewhere.<\/li>\n<\/ul>\n\n<h4>The dev-to-live problem this solves<\/h4>\n\n<p>When you copy or migrate WordPress content between environments - for example from a <strong>dev or staging<\/strong> site to your <strong>live<\/strong> site - images embedded in that content stay <em>hotlinked<\/em> to the source domain. The block HTML (or pasted classic-editor markup) keeps pointing at <code>https:\/\/dev.example.com\/...<\/code>, so your production pages silently load images from staging. If the source environment is private, password-protected or taken down after launch, those images break.<\/p>\n\n<p>This plugin fixes that automatically: it finds the external image URLs, pulls the files into your own Media Library, and updates the content so everything is served from your own domain.<\/p>\n\n<h4>Key features<\/h4>\n\n<ul>\n<li><strong>Safe imports.<\/strong> Every download is verified to be a genuine, decodable image of the expected type before any URL is rewritten. If a file fails that check - for example the \"image\" was really a geo-blocked placeholder page or an error document - it is discarded and your original reference is left unchanged, so a broken file is never put in place of a working URL.<\/li>\n<li><strong>Self-hosted images.<\/strong> Serving images from your own domain means search engines index them on your domain and your pages no longer depend on a third party staying online.<\/li>\n<li><strong>Full-size originals.<\/strong> It imports the full-size original and lets WordPress regenerate every registered size locally, then points the markup at the matching local size so the layout is unchanged.<\/li>\n<li><strong>Duplicate-aware.<\/strong> Images that already exist in your Media Library (by source URL or exact filename) are reused instead of downloaded again.<\/li>\n<li><strong>Zero configuration.<\/strong> It works on activation; there is nothing to set up.<\/li>\n<\/ul>\n\n<h4>Security<\/h4>\n\n<p>Server-side remote fetching is handled with defence in depth:<\/p>\n\n<ul>\n<li><strong>SSRF guard on every fetch.<\/strong> Before any download, the target URL is validated. Requests to private, reserved, loopback, link-local, ULA, CGNAT and IPv6 transition (6to4 \/ NAT64) ranges are blocked - including the cloud metadata endpoint <code>169.254.169.254<\/code>. <em>All<\/em> resolved A\/AAAA records must be public.<\/li>\n<li><strong>IP pinning and redirect re-validation.<\/strong> The validated public IP is <em>pinned<\/em> for the actual download (cURL transport), and redirects are followed manually with the SSRF guard re-run on every hop. A public, validator-passing URL therefore cannot be DNS-rebound or 30x-redirected to an internal address after the check.<\/li>\n<li><strong>Scheme, port and credential locking.<\/strong> Only <code>http<\/code> and <code>https<\/code>, only ports <code>80<\/code> and <code>443<\/code>, and URLs containing embedded credentials (<code>user:pass@<\/code>) are refused.<\/li>\n<li><strong>Real content-type verification.<\/strong> After download the <em>actual<\/em> file type is checked, not just the extension. Anything that is not a genuine raster image is deleted and rejected.<\/li>\n<li><strong>SVG and ICO are skipped by default.<\/strong> SVG can carry script and ICO is a common smuggling vector, so both are excluded.<\/li>\n<li><strong>Download caps.<\/strong> A per-request timeout and a maximum file size are enforced (and filterable) so a hostile or runaway source cannot tie up or fill your server.<\/li>\n<\/ul>\n\n<h4>How it works<\/h4>\n\n<p>Paste your content as you normally would, then Publish or Save. When you do, the plugin scans the content for externally-hosted images, securely downloads each one into your Media Library, and rewrites the markup to your local copy at the same size it referenced, so your layout is unchanged.<\/p>\n\n<ul>\n<li><strong>Automatically on save.<\/strong> A single canonical save hook covers <em>both<\/em> the Block (Gutenberg) editor and the Classic editor, so newly pasted external images are imported the moment you save.<\/li>\n<li><strong>It skips duplicates.<\/strong> Before downloading, it checks whether the image already exists in your Media Library - by the exact remote source URL it previously imported, and by exact filename against your whole Media Library (including images you uploaded normally). If a match exists it is reused instead of downloaded again.<\/li>\n<li><strong>It imports the full-size original.<\/strong> Even when your content references a scaled image (for example <code>photo-1024x670.webp<\/code>), the plugin fetches the full original and lets WordPress regenerate every registered size locally, then points the markup at the matching local size.<\/li>\n<li><strong>WP-CLI for retroactive fixes.<\/strong> Run <code>wp auei localise<\/code> to sweep content that was saved <em>before<\/em> the plugin was active, with <code>--dry-run<\/code>, <code>--post_type<\/code>, <code>--post_id<\/code> and <code>--limit<\/code> options.<\/li>\n<\/ul>\n\n<h4>What it scans<\/h4>\n\n<ul>\n<li><code>src<\/code>, <code>data-src<\/code> and <code>data-lazy-src<\/code> attributes<\/li>\n<li><code>srcset<\/code> (every candidate, with width and density descriptors)<\/li>\n<li>Gutenberg block-comment JSON (<code>url<\/code> \/ <code>src<\/code> \/ <code>href<\/code>)<\/li>\n<li>CSS <code>background-image: url(...)<\/code> in inline styles<\/li>\n<\/ul>\n\n<h4>Compatibility<\/h4>\n\n<ul>\n<li><strong>Block and Classic editors.<\/strong> A single save hook (<code>wp_insert_post_data<\/code>) covers both.<\/li>\n<li><strong>Single sites and multisite.<\/strong> On multisite, each site's external images are imported into that site's own Media Library.<\/li>\n<li><strong>Alt text and captions are preserved.<\/strong> The plugin only rewrites image URLs and remaps block attachment IDs; your alt text, captions and other attributes are left untouched.<\/li>\n<li><strong>Supported formats:<\/strong> JPEG, PNG, GIF, WebP, AVIF and BMP. SVG and ICO are excluded by default for security.<\/li>\n<li><strong>Actively maintained<\/strong> and tested against the current WordPress release.<\/li>\n<\/ul>\n\n<h4>Privacy and external connections<\/h4>\n\n<p>This plugin <strong>does not<\/strong> phone home and <strong>does not<\/strong> send your data to any third party. There are no analytics, no tracking and no external API calls. The <em>only<\/em> outbound requests it makes are to the image URLs that already appear in <em>your own<\/em> content, purely to download those images into your Media Library. Nothing about your site, content or visitors is transmitted anywhere else.<\/p>\n\n<h3>Filters and Hooks<\/h3>\n\n<p>All filters use the <code>auei_<\/code> prefix.<\/p>\n\n<ul>\n<li><code>auei_enabled<\/code> (bool, default <code>true<\/code>) - master on\/off switch for the whole plugin.<\/li>\n<li><code>auei_whitelisted_domains<\/code> (string[], default empty) - domains whose images are never imported (left as external), for example a trusted CDN.<\/li>\n<li><code>auei_import_full_size<\/code> (bool, default <code>true<\/code>) - import the full-size original (strip a <code>-WxH<\/code> scaled suffix) so the destination regenerates every size. Set to false to import only the exact file referenced.<\/li>\n<li><code>auei_remap_ids<\/code> (bool, default <code>true<\/code>) - rewrite the source-site attachment ID in Gutenberg blocks (<code>\"id\":N<\/code> \/ <code>wp-image-N<\/code>) to the new local attachment ID.<\/li>\n<li><code>auei_allowed_extensions<\/code> (string[], default <code>jpg, jpeg, png, gif, webp, avif, bmp<\/code>) - image extensions eligible for import. <code>svg<\/code> and <code>ico<\/code> are excluded by default.<\/li>\n<li><code>auei_allowed_schemes<\/code> (string[], default <code>http, https<\/code>) - URL schemes permitted for fetching.<\/li>\n<li><code>auei_allowed_ports<\/code> (int[], default <code>80, 443<\/code>) - explicit ports permitted on a target URL.<\/li>\n<li><code>auei_blocked_ip_ranges<\/code> (string[], default empty) - extra CIDR ranges to block, in addition to the built-in private\/reserved set.<\/li>\n<li><code>auei_max_file_size<\/code> (int, default <code>8388608<\/code>, i.e. 8 MB) - maximum download size in bytes; larger files are rejected.<\/li>\n<li><code>auei_download_timeout<\/code> (int, default <code>20<\/code>) - download timeout in seconds.<\/li>\n<li><code>auei_max_redirects<\/code> (int, default <code>3<\/code>) - maximum HTTP redirects to follow; every hop is re-validated by the SSRF guard.<\/li>\n<li><code>auei_reuse_by_filename<\/code> (bool, default <code>true<\/code>) - reuse an existing Media Library image with the same filename (any attachment, not just ones this plugin imported) instead of downloading a duplicate. Matched on the exact stored-file basename.<\/li>\n<li><code>auei_allow_url<\/code> (true or WP_Error, default validator result) - final override on a per-URL basis; return <code>true<\/code> to force-allow or a <code>WP_Error<\/code> to force-block.<\/li>\n<li><code>auei_force_logging<\/code> (bool, default <code>false<\/code>) - force debug logging even when <code>WP_DEBUG<\/code> is off.<\/li>\n<\/ul>\n\n<!--section=installation-->\n<ol>\n<li>Upload the <code>nerdcow-external-image-importer<\/code> folder to <code>wp-content\/plugins\/<\/code>, or install the plugin through the <strong>Plugins &gt; Add New<\/strong> screen.<\/li>\n<li>Activate the plugin through the <strong>Plugins<\/strong> screen in WordPress.<\/li>\n<li>That is it - no configuration is required. External images are imported automatically the next time you save a post or page.<\/li>\n<li>(Optional) To fix content saved before activation, run the WP-CLI command: <code>wp auei localise --dry-run<\/code> to preview, then <code>wp auei localise<\/code> to apply.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"will%20it%20ever%20replace%20a%20working%20image%20with%20a%20broken%20one%3F\"><h3>Will it ever replace a working image with a broken one?<\/h3><\/dt>\n<dd><p>No - this is a core safety behaviour. Every download is verified to be a genuine, decodable image of the expected MIME type before any URL is rewritten. If the downloaded file is not a real image (for example a geo-blocked placeholder page or an error document), it is discarded and your original external URL is left exactly as it was. A broken local copy is never put in place of a URL that was working.<\/p><\/dd>\n<dt id=\"does%20it%20work%20with%20the%20block%20editor%20and%20the%20classic%20editor%3F\"><h3>Does it work with the Block editor and the Classic editor?<\/h3><\/dt>\n<dd><p>Yes. A single save hook (<code>wp_insert_post_data<\/code>) covers both the Block (Gutenberg) editor - which saves via the REST API - and the Classic editor.<\/p><\/dd>\n<dt id=\"does%20it%20work%20on%20multisite%3F\"><h3>Does it work on multisite?<\/h3><\/dt>\n<dd><p>Yes. It runs per site, so each site's external images are imported into that site's own Media Library.<\/p><\/dd>\n<dt id=\"what%20happens%20to%20alt%20text%20and%20captions%3F\"><h3>What happens to alt text and captions?<\/h3><\/dt>\n<dd><p>They are preserved. The plugin only rewrites image URLs and remaps the attachment ID stored in Gutenberg blocks; it does not touch your alt text, captions or any other attributes.<\/p><\/dd>\n<dt id=\"which%20image%20formats%20are%20supported%3F\"><h3>Which image formats are supported?<\/h3><\/dt>\n<dd><p>JPEG, PNG, GIF, WebP, AVIF and BMP. SVG and ICO are excluded by default for security (SVG can embed script, and ICO is a common smuggling vector). If you fully trust your sources you can extend the allowed set via the <code>auei_allowed_extensions<\/code> filter, but doing so is at your own risk.<\/p><\/dd>\n<dt id=\"does%20it%20send%20my%20data%20anywhere%3F\"><h3>Does it send my data anywhere?<\/h3><\/dt>\n<dd><p>No. The plugin uses no external services and contains no tracking or analytics. The <em>only<\/em> outbound HTTP requests it makes are to the image URLs that already exist in your own content, purely to download those images into your local Media Library.<\/p><\/dd>\n<dt id=\"which%20image%20size%20does%20it%20import%2C%20and%20what%20happens%20to%20the%20block%20in%20the%20editor%3F\"><h3>Which image size does it import, and what happens to the block in the editor?<\/h3><\/dt>\n<dd><p>It imports the <strong>full-size original<\/strong>. WordPress content usually references a <em>scaled<\/em> image (for example <code>photo-1024x670.webp<\/code>); the plugin strips that <code>-WxH<\/code> suffix, fetches the full original, and lets WordPress regenerate every registered size locally - so you have the thumbnail, medium, large and full versions, just like a normal upload. The image in your content is then rewritten to the local copy at the <strong>same size it referenced<\/strong>, so the layout is unchanged. It also remaps the attachment ID stored in the block (<code>\"id\":N<\/code> \/ <code>wp-image-N<\/code>) to the new local attachment, so the editor's image controls keep working and no stale or colliding source ID is left behind. Both behaviours can be turned off with the <code>auei_import_full_size<\/code> and <code>auei_remap_ids<\/code> filters.<\/p><\/dd>\n<dt id=\"will%20it%20re-download%20images%20it%20has%20already%20imported%3F\"><h3>Will it re-download images it has already imported?<\/h3><\/dt>\n<dd><p>No. Before downloading, the plugin checks whether the image already exists in your Media Library and reuses it:<\/p>\n\n<ol>\n<li>by the exact remote source URL it previously imported (stored in <code>_auei_source_url<\/code> post meta); and<\/li>\n<li>by exact filename against your <em>whole<\/em> Media Library, including images you uploaded normally, not just ones this plugin imported.<\/li>\n<\/ol>\n\n<p>Point 2 is the important one for the dev-to-live workflow: if the destination already has the images (for example the page was partly built on live before), they are reused instead of re-uploaded as duplicates. Matching is on the exact stored-file basename, and only when a single unambiguous match exists, so an unrelated image is never picked. Turn filename reuse off with the <code>auei_reuse_by_filename<\/code> filter if you prefer always-fresh imports.<\/p><\/dd>\n<dt id=\"how%20do%20i%20keep%20images%20from%20a%20trusted%20cdn%20or%20partner%20as%20external%3F\"><h3>How do I keep images from a trusted CDN or partner as external?<\/h3><\/dt>\n<dd><p>Add the domain to the whitelist with the <code>auei_whitelisted_domains<\/code> filter. Whitelisted domains are never imported and their URLs are left untouched.<\/p><\/dd>\n<dt id=\"is%20it%20safe%20to%20run%20on%20a%20server%20behind%20a%20private%20network%3F\"><h3>Is it safe to run on a server behind a private network?<\/h3><\/dt>\n<dd><p>Yes - that is a core design goal. The built-in SSRF guard blocks requests to private, reserved, loopback, link-local, ULA and CGNAT addresses, plus the cloud metadata endpoint, and requires every resolved IP of a host to be public before any fetch happens.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.0.1<\/h4>\n\n<ul>\n<li>Reuse existing Media Library images by exact filename - including images uploaded normally, not just ones this plugin imported - so copying a page whose images already exist on the destination no longer creates duplicate uploads. Matched on the exact stored-file basename, single unambiguous match only.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial release.<\/li>\n<li>Automatic import of externally-hosted images on post save (Block and Classic editors).<\/li>\n<li>Imports the full-size original (regenerating every thumbnail and size locally) even when the content references a scaled \"-WxH\" image, then rewrites the markup to the matching local size so the layout is unchanged.<\/li>\n<li>Remaps the source-site attachment ID carried in Gutenberg image, cover and gallery blocks (<code>\"id\":N<\/code> \/ <code>wp-image-N<\/code>) to the new local attachment ID, so blocks never point at a stale or colliding ID on the destination.<\/li>\n<li>SSRF-safe URL validation: blocks private, reserved, loopback, link-local, ULA, CGNAT, IPv6 6to4\/NAT64 and cloud-metadata addresses; requires all resolved IPs to be public; pins the validated IP for the fetch and re-validates every redirect hop; locks scheme, port and credentials.<\/li>\n<li>Real content-type verification after download; SVG and ICO excluded by default.<\/li>\n<li>Download timeout and maximum file-size caps (filterable).<\/li>\n<li>Attachment reuse by <code>_auei_source_url<\/code> meta and (optionally) by previously-imported filename to avoid re-downloading.<\/li>\n<li>URL rewriting across <code>src<\/code>, <code>data-src<\/code>, <code>data-lazy-src<\/code>, <code>srcset<\/code>, Gutenberg block JSON and CSS <code>background-image<\/code>.<\/li>\n<li><code>wp auei localise<\/code> WP-CLI command with <code>--dry-run<\/code>, <code>--post_type<\/code>, <code>--post_id<\/code> and <code>--limit<\/code> options for retroactive fixes.<\/li>\n<li>Comprehensive <code>auei_*<\/code> filter set for whitelisting, allowed types, schemes and ports, caps and overrides.<\/li>\n<\/ul>","raw_excerpt":"Automatically import external and remote images into your Media Library on save and rewrite the URLs to local copies. Built SSRF-safe.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/336996","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=336996"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/nerdcow"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=336996"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=336996"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=336996"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=336996"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=336996"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=336996"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}