Some WordPress plugins do exactly one useful thing, do it well, and then quietly disappear. DX Auto Save Images was one of those plugins. Originally released by “大侠wp” (daxiawp), it solved a genuine problem: when you paste an article into WordPress with externally-hosted images, those images can vanish at any time — the source server goes offline, changes its URL structure, or blocks hotlinking. The plugin automatically downloaded those remote images to your own server and replaced the URLs in the post content, silently, on save.
It was last updated twelve years ago. It was closed on WordPress.org. In 2026, I still needed it on several projects, so I fixed it.
What the Plugin Does
DX Auto Save Images hooks into the post-save process and:
- Scans post content for
<img>tags with externalsrcURLs - Downloads each image to the local WordPress uploads directory
- Registers each image as a proper WordPress media attachment
- Replaces the remote URL in the post content with the new local URL
- Optionally sets the first downloaded image as the post’s featured image
It’s a “set and forget” plugin. You paste content, hit publish, and your images are now self-hosted.
What Was Broken
Three separate problems made the plugin completely non-functional on a modern WordPress stack.
1. The Block Editor Killed It
This was the critical one. The plugin registered its entire image-saving logic on the content_save_pre filter:
add_filter( 'content_save_pre', array( $this, 'post_save_images' ) );
This filter fires when a post is saved through the classic editor — a standard HTML form POST request to wp-admin/post.php. Since WordPress 5.0, the block editor (Gutenberg) has been the default, and it saves posts through the REST API. The content_save_pre filter is never called during a REST API save.
The plugin’s entire image-saving logic was being silently skipped every single time anyone saved a post in the block editor. No errors, no warnings — just nothing happening.
2. PHP 8.x Compatibility
PHP 8.0 changed the behavior of accessing undefined array keys: instead of returning null silently, it now raises an E_WARNING. The plugin had several of these throughout:
// Raises "Undefined array key" warnings in PHP 8.x
if ( $_POST['save'] || $_POST['publish'] ) { ... }
if ( $_POST['submit'] ) { ... }
if ( $options['chinese'] == 'yes' ) { ... }
The $options variable comes from get_option(), which returns false when the option doesn’t exist yet (first install, before any settings have been saved). Accessing array keys on false throws additional warnings.
On top of that, images were downloaded using file_get_contents() with no error handling, no timeout, and no validation of what came back.
3. A Reported CSRF Vulnerability
A Cross-Site Request Forgery vulnerability was publicly reported against this plugin and assigned CVE-2023-40671 (CVSS 4.3) in August 2023. The plugin’s settings form had no nonce field, meaning a malicious third-party page could silently trick a logged-in administrator into submitting the form and changing plugin settings. The vulnerability was marked unpatched — because the plugin was abandoned.
The Fixes
Fix 1: Switch Hook to wp_insert_post_data
The correct hook for intercepting and modifying post content before it hits the database — regardless of whether the save comes from the classic editor or the block editor REST API — is wp_insert_post_data. It fires inside wp_insert_post(), which is the single function both editors ultimately call.
// Before
add_filter( 'content_save_pre', array( $this, 'post_save_images' ) );
// After
add_filter( 'wp_insert_post_data', array( $this, 'post_save_images' ), 10, 2 );
The callback signature changes from receiving $content directly to receiving the full $data array and the raw $postarr array. Post content is read from and written back to $data['post_content']. Autosaves, auto-drafts, and revisions are explicitly skipped to avoid triggering unnecessary downloads:
function post_save_images( $data, $postarr ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return $data;
if ( isset( $data['post_status'] ) && $data['post_status'] === 'auto-draft' ) return $data;
if ( isset( $postarr['post_type'] ) && $postarr['post_type'] === 'revision' ) return $data;
// ... process $data['post_content'] ...
return $data;
}
Fix 2: PHP 8.x Compatibility
Every $_POST access now uses isset(), and every value from get_option() is validated with is_array() before any key is accessed:
// Before
if ( $_POST['submit'] ) { ... }
// After
if ( isset( $_POST['submit'] ) && $_POST['submit'] ) {
check_admin_referer( 'dx_auto_save_images_options', 'dx_nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'You do not have permission to change these settings.' );
}
// ...
}
// Before
if ( $options['tmb'] == 'yes' ) { ... }
// After
$options = get_option( 'dx-auto-save-images-options' );
if ( is_array( $options ) && ! empty( $options['tmb'] ) && $options['tmb'] === 'yes' ) { ... }
Fix 3: CSRF Nonce (CVE-2023-40671)
A WordPress nonce is added to the settings form and verified before any data is written. This is the standard two-line fix for this class of vulnerability.
In options-form.php, inside the <form> tag:
<?php wp_nonce_field( 'dx_auto_save_images_options', 'dx_nonce' ); ?>
In save_options(), before touching any $_POST data:
check_admin_referer( 'dx_auto_save_images_options', 'dx_nonce' );
check_admin_referer() verifies the nonce and calls wp_die() with a 403 if the nonce is missing, expired, or invalid — blocking any forged cross-site request. A current_user_can( 'manage_options' ) check is added as a second layer to ensure only administrators can save settings.
Fix 4: Better Image Downloading
file_get_contents() was replaced with WordPress’s built-in HTTP API, wp_remote_get(). This respects the site’s proxy configuration, applies proper timeouts, and returns a structured response object. Three validation layers were added on top:
// Block non-HTTP(S) schemes: file://, data:, ftp://, etc.
if ( ! wp_http_validate_url( $image_url ) ) return [ 'url' => $image_url ];
$response = wp_remote_get( $image_url, [ 'timeout' => 30 ] );
if ( is_wp_error( $response ) ) return [ 'url' => $image_url ];
// Only accept a clean 200 — not a 301 that leads to an error page
if ( wp_remote_retrieve_response_code( $response ) !== 200 ) return [ 'url' => $image_url ];
// Reject non-image responses: HTML error pages, JSON, redirects to login, etc.
$content_type = wp_remote_retrieve_header( $response, 'content-type' );
if ( strpos( $content_type, 'image/' ) === false ) return [ 'url' => $image_url ];
If any check fails, the original URL is returned untouched — the post saves normally, nothing is corrupted.
Fix 5: Universal Filename Handling
The original plugin had an option called “Chinese filename support” that renamed image files to their MD5 hash before saving — a blunt workaround for uploads failing on non-ASCII filenames. That option was removed entirely and replaced with a proper universal approach using WordPress’s own sanitize_file_name():
// URL-decode first (resolves %E4%B8%AD%E6%96%87-style encoding in URLs)
$raw_name = urldecode( basename( parse_url( $image_url, PHP_URL_PATH ) ) );
// sanitize_file_name() calls remove_accents() for UTF-8 transliteration,
// strips special characters, and converts spaces to hyphens
$filename = sanitize_file_name( $raw_name );
// If sanitization stripped everything (e.g. a filename of pure symbols or
// characters with no ASCII equivalent), fall back to a hash of the URL
if ( empty( pathinfo( $filename, PATHINFO_FILENAME ) ) ) {
$ext = pathinfo( $raw_name, PATHINFO_EXTENSION );
$filename = md5( $image_url ) . ( $ext ? '.' . sanitize_file_name( $ext ) : '' );
}
This handles any filename — Chinese, Arabic, Cyrillic, emoji, spaces, special characters — without losing readability when the filename can be preserved.
Fix 6: Block Editor Per-Post Toggle
The original plugin had a per-post checkbox (shown via the submitpost_box action) to skip image downloading for a specific post. This hook only fires in the classic editor — invisible in the block editor.
The fix has two parts. First, the skip preference is registered as proper post meta exposed to the REST API:
register_post_meta( '', '_dx_skip_remote_images', [
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'auth_callback' => function() { return current_user_can( 'edit_posts' ); },
] );
Second, a small JavaScript file registers a toggle control in the block editor’s Status & Visibility sidebar panel, wired directly to that meta field:
wp.plugins.registerPlugin( 'dx-auto-save-images-toggle', {
render: function () {
var meta = wp.data.useSelect( function ( select ) {
return select( 'core/editor' ).getEditedPostAttribute( 'meta' ) || {};
} );
var editPost = wp.data.useDispatch( 'core/editor' ).editPost;
return wp.element.createElement( wp.editPost.PluginPostStatusInfo, null,
wp.element.createElement( wp.components.ToggleControl, {
label: 'Skip remote image download for this post',
checked: meta._dx_skip_remote_images === 'yes',
onChange: function ( value ) {
editPost( { meta: { _dx_skip_remote_images: value ? 'yes' : '' } } );
}
} )
);
}
} );
No build step required — it uses the wp.* globals already loaded by the block editor.
Fix 7: Cleanup
- Removed the Theme Shop submenu. The original plugin added a submenu page that loaded third-party ad scripts from Baidu’s CDN. That entire menu item and its companion
theme.phpfile were removed. - Translated all UI to English. Every label, description, and admin string was in Chinese. All translated to
en_US. - Improved settings descriptions. Each option now has an inline explanation of what it actually does.
Summary of Changes
| Problem | Root Cause | Fix |
|---|---|---|
| Images not saved in block editor | content_save_pre not fired by REST API | Switched to wp_insert_post_data |
| PHP 8.x warnings / errors | Direct $_POST and $options access without isset() | Added isset() and is_array() guards throughout |
| CSRF vulnerability (CVE-2023-40671) | No nonce on settings form | wp_nonce_field() + check_admin_referer() + current_user_can() |
| Unreliable image downloads | file_get_contents() with no error handling | wp_remote_get() with HTTP status and content-type validation |
| Non-ASCII filename failures | Manual MD5 rename option | urldecode() + sanitize_file_name() + hash fallback |
| Per-post toggle missing in block editor | submitpost_box is classic editor only | Registered post meta + block editor sidebar JS panel |
| Embedded third-party ad scripts | Theme Shop submenu loaded Baidu CDN scripts | Removed entirely |
The Updated Plugin
The plugin is now at version 1.6.0 and working on WordPress 6.9.4 with PHP 8.5. The core behaviour is unchanged — it still silently downloads and re-hosts remote images on post save — but it now works correctly with the block editor, handles every failure case gracefully, and is no longer a CSRF risk.
If you are running the original 1.4.0 or earlier, your site’s plugin settings page is exposed to CVE-2023-40671. Update or deactivate.
The updated plugin is available at GitHub.