External Images security review

Security › External Images

Security score 100/100 · Version 3.43.20 · Reviewed 3 November 2026 · Source fingerprint 6ca0ca0

Summary

We reviewed External Images 3.43.20 on 3 November 2026, and it scored 100/100 for security. The review was carried out by an AI agent working through our written security checklist, and over the day it read every first-party PHP file, template and JavaScript file in full. It ran seven times, each time on the latest fixed version, and the scores were 0, 50, 75, 85, 75, 80 and 100 as each round looked further. Each finding was reproduced on a private test site, including a real multisite network and a WooCommerce store, and each fix was confirmed with a test that failed before the change and passes after it.

What was found and fixed

Each review round numbered its own findings from SECURITY-001, so the IDs below give the round and the version reviewed.

  • Round 1 (3.43.13) SECURITY-001, Critical, 25 points. Gallery image URLs entered in the post editor were saved and printed back into the gallery meta box without cleaning or escaping. An Author or Contributor could save a URL that ran script when an editor or administrator opened the post in wp-admin. The product variation gallery had the same gap. Fix: gallery URLs are cleaned as URLs when saved, and entries that are not valid URLs are dropped. Every value in both gallery meta boxes is escaped when printed, which also covers values saved earlier. 3.43.15 also escaped the variation image URL field. Fixed in: 3.43.14.
  • Round 1 (3.43.13) SECURITY-002, Critical, 25 points. The import fields such as external_image_url are ordinary custom fields that anyone who can edit a post can set. Their value was copied unchanged into the image record and printed without escaping in the Open Graph and Twitter image tags and in image elements. An Author could run script for every visitor to the post, administrators included. Fix: every imported URL is cleaned when it is mapped, and every image URL is escaped wherever it is printed, including the admin columns. Output escaping also protects values imported earlier. Fixed in: 3.43.14.
  • Round 1 (3.43.13) SECURITY-003, High, 25 points. The settings were saved without type checks, and the crop height was printed inside a style element on every page. On multisite, where a sub-site administrator is not allowed to add script, a sub-site administrator could run script for the network super administrator and every visitor to that sub-site. Single-site installs were not affected. Fix: every setting is checked against its expected type when saved and unknown keys are dropped, and the crop height is cast to a number when printed. Fixed in: 3.43.14.
  • Round 1 (3.43.13) SECURITY-004, High, 25 points. With Proxy All Images or Proxy HTTP Images on (both off by default), the image proxy would fetch from any address, including the server’s own loopback and private network, and it cached SVG images, which can contain script, on the site’s own domain. Fix: the proxy uses WordPress’s safe request function, which refuses private and loopback addresses; SVG images are no longer proxied and load from their original address; previously cached SVG files are deleted on upgrade; and the HTTPS check skips addresses WordPress rejects. Fixed in: 3.43.14.
  • Round 2 (3.43.15) SECURITY-001, High, 25 points. Import image titles and captions were also ordinary custom fields. WordPress filters these for users who may not add script, but the mapping can run during a later request by an administrator, and then the text was stored unfiltered. An Author could arrange for script to be stored in an image title that shows on its attachment page. Fix: import titles, alt text and captions are cleaned whichever user’s request runs the mapping. Fixed in: 3.43.16.
  • Round 2 (3.43.15) SECURITY-002, Medium, 25 points. With the Datafeedr Check Image Sizes option set to pick the largest image (off by default), the plugin measured an image from the merchant_image_url custom field at any address, so an Author could make the server request addresses on its private network. Fix: only web addresses that WordPress accepts as public are measured. Fixed in: 3.43.16.
  • Round 3 (3.43.16) SECURITY-001, Medium, 25 points. When an image URL matched an existing media library item, the plugin reused that item and rewrote its title, alt text and size details, whoever owned it. An Author could break or retitle another user’s uploads by entering their address. Fix: only attachments the plugin itself created are reused; a URL that matches an ordinary upload gets its own external image record. Fixed in: 3.43.17.
  • Round 4 (3.43.17) SECURITY-001, Low, 15 points. The public request that loads a product variation’s images checked that the product was published but not whether it was password-protected, so anyone could list the images of a protected product. Fix: the request is refused for a password-protected product. Fixed in: 3.43.18.
  • Round 5 (3.43.18) SECURITY-001, Medium, 25 points. The verify-images AI ability checks stored image addresses, including ones an Author can set, and it would request any address. When an administrator or their AI agent ran the check, an Author’s address could become a request into the server’s private network, with its status code reported back. Fix: the ability uses WordPress’s safe request function, which also checks every redirect. Fixed in: 3.43.19.
  • Round 6 (3.43.19) SECURITY-001, Low, 20 points. One small file of constants had no direct-access guard, and the main plugin file loaded the settings library before its own guard, so requesting it directly caused a server error that could show the site’s file path on servers set to display errors. Fix: every PHP file in the plugin now stops immediately when opened directly. Fixed in: 3.43.20.

Version 3.43.18 also changed how the wp-admin thumbnail preview for spreadsheet-style bulk editors is built, setting the image address as an attribute rather than as HTML. Round 4 had noted that path but could not test it without a third-party bulk editor, so it was not scored.

The 90/100 published on 4 September 2026 reflected one finding in the shared licensing and update library that all our paid plugins carry:

  • Licence key in update URLs, Medium, 10 points. Update downloads included the licence key in the download URL, where web servers, proxies and WordPress’s own update records could store it. Fix: the licence key is now sent in a request header that goes only to our update server, with redirects turned off. Fixed in: 3.43.12 (shared library 1.14.3). Version 3.43.13 also includes shared library 1.14.5, which stops the AI connection set-up from creating a user unless the person setting it up is allowed to add users, and no longer includes a developer build script from the bundled update checker.

What the review covered

AreaResultEvidence
Authentication, authorisation, ownership and request forgeryCheckedSave handlers check edit_post behind WordPress’s and WooCommerce’s nonces. The HTTPS capability update needs a nonce and manage_options, and the AI abilities need ei_manage. The one public ajax request checks post type, status and password. Attachment reuse is limited to attachments the plugin created.
SQL injectionCheckedEvery query is prepared. Lists placed in queries are integers or database IDs passed through esc_sql(), and the verify-images limit is an integer.
Cross-site scriptingCheckedImage URLs are cleaned on every save path and escaped at every output: Open Graph and Twitter tags, image elements, both gallery meta boxes and the admin columns. Settings are type-checked and cast at output. Import titles, alt text and captions are cleaned. No SVG is proxied, and the admin JavaScript builds no HTML from stored values.
Server-side request forgery, open redirects and header injectionCheckedThe proxy and verify-images use WordPress’s safe request functions. The HTTPS check and the Datafeedr size check first pass the address through wp_http_validate_url().
File uploads and path traversalCheckedThe proxy writes only PNG, JPEG and GIF files, named from the file name plus a hash. Previously cached SVG files are removed on upgrade.
Remote code execution and unsafe deserialisationCheckedNo unserialize of input, no dynamic includes and no eval. Image size checks accept only http and https addresses.
XML parsingNot applicableThe plugin does not parse XML or SVG.
Secrets, privacy and data exposureCheckedImages of password-protected products are withheld from the public request. The licence key is handled by the shared settings library.
Business logic, replay and concurrencyCheckedFront-end conversion writes only the post’s own attachments, and clean-up deletes only external image attachments attached to that post.
DependenciesCheckedShared settings library 1.14.5, with no known advisories.
JWT, GraphQL and server-side templatesNot applicableThe plugin uses none of these. GraphQL appears only as a passive filter for Shopz.io.

Not scored

  • Woodmart variation images (unverified). Variation image addresses reach the Woodmart theme as JSON-encoded values. They have been cleaned when saved since 3.43.14; values saved before then through the variation gallery fields could only have been entered by shop managers and administrators. How Woodmart prints them was not tested, because the review site did not run the Woodmart theme.
  • Shared external images. By design, one external image record can be shared by several products that use the same image address, so editing one product’s image text can change the text shown for that image on the others. The text is cleaned, and the record belongs to the plugin’s import data, not to any user’s upload.

Back to all security scores

▲
▼
1/1