Root Cause Diagnosis
WooCommerce builds SVG icon URLs (payment icons, cart and checkout block assets, theme sprites) with plugins_url(), WC()->plugin_url(), and get_template_directory_uri(), which choose http:// or https:// based on is_ssl(). With Cloudflare SSL mode set to Flexible, the visitor connects over HTTPS but Cloudflare fetches from the origin over plain HTTP, so PHP sees no HTTPS server variable and is_ssl() returns false. Every asset URL generated at runtime is then emitted as http://, and the browser blocks or warns on the SVG requests. The same warning appears when home and siteurl are still stored as http:// in the database, or when absolute http:// URLs are cached in generated CSS or page-builder data.
Quick Fix
Make PHP detect HTTPS from the header Cloudflare forwards. Add this to wp-config.php above the line /* That's all, stop editing! */:
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
Then purge the Cloudflare cache and any page cache.
Step-by-Step Resolution
1. Locate the offending URLs
Find the exact http:// references before changing anything:
curl -s https://example.com/checkout/ | grep -oE "(src|href|data|xlink:href)=\"http://[^\"]+\.svg[^\"]*\""
curl -s https://example.com/checkout/ | grep -oE "url\(http://[^)]+\.svg\)"
In the browser, open DevTools > Console and filter for Mixed Content. The message includes the blocked URL and its initiator.
2. Move Cloudflare from Flexible to Full (strict)
In Cloudflare Dashboard > SSL/TLS > Overview, set the mode to Full (strict). This requires a valid certificate on the origin (a Let’s Encrypt or Cloudflare Origin CA certificate) and a listen 443 ssl block on the origin web server. With HTTPS end to end, the origin sets HTTPS=on natively and step 3 becomes a fallback.
3. Pass the forwarded protocol to PHP
Needed when the origin still receives HTTP from Cloudflare (Flexible mode, Cloudflare Tunnel, or a reverse proxy in front of the origin).
Nginx (/etc/nginx/conf.d/example.com.conf or your vhost):
map $http_x_forwarded_proto $https_forwarded {
default $https;
https on;
}
server {
# ...
location ~ \.php$ {
include fastcgi_params;
fastcgi_param HTTPS $https_forwarded;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
}
The map block goes in the http context (a file in /etc/nginx/conf.d/ is included there).
Apache (.htaccess or the vhost):
SetEnvIf X-Forwarded-Proto "^https$" HTTPS=on
Security note: X-Forwarded-Proto is client-controllable if the origin is reachable directly. Restrict origin ports 80 and 443 to Cloudflare’s published IP ranges (https://www.cloudflare.com/ips/) at the firewall or in Nginx.
4. Correct the stored site URLs
wp option get home
wp option get siteurl
wp option update home 'https://example.com'
wp option update siteurl 'https://example.com'
Check wp-config.php for WP_HOME and WP_SITEURL constants, which override the database values:
define('WP_HOME', 'https://example.com');
define('WP_SITEURL', 'https://example.com');
5. Replace http:// URLs in the database
Always dry-run first. WP-CLI handles serialized data correctly, and plain SQL REPLACE() does not.
wp db export backup-$(date +%F).sql
wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --dry-run
wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid
Page builders (Elementor, Divi, Bricks) store JSON with escaped slashes in wp_postmeta. Run a second pass for that form:
wp search-replace 'http:\/\/example.com' 'https:\/\/example.com' --all-tables --skip-columns=guid
6. Fix hardcoded URLs in theme and plugin files
grep -rnE "http://[^\"' ]+\.svg" wp-content/themes/ wp-content/mu-plugins/ --include=*.php --include=*.css --include=*.js
Replace hardcoded absolute URLs with protocol-aware helpers such as get_stylesheet_directory_uri(), plugins_url(), and wp_get_attachment_url(). For unavoidable static references, use root-relative paths (/wp-content/...). In a child theme, override the affected template instead of editing the parent.
7. Regenerate cached assets
Absolute URLs are baked into generated files and caches:
wp cache flush
wp transient delete --all
wp elementor flush-css # Elementor only
Then:
- WooCommerce > Status > Tools > Clear transients
- Purge WP Rocket, LiteSpeed Cache, W3 Total Cache, or your page cache plugin
- Cloudflare Dashboard > Caching > Configuration > Purge Everything
8. Enable Cloudflare HTTPS enforcement
In Cloudflare Dashboard > SSL/TLS > Edge Certificates:
- Always Use HTTPS: On
- Automatic HTTPS Rewrites: On
Automatic HTTPS Rewrites only rewrites http:// references in HTML that it can confirm are available over HTTPS. It does not rewrite URLs inside external CSS or JS files, JSON payloads, or dynamically injected markup, so it is a safety net and not a fix.
9. Optional: add a CSP upgrade header
add_header Content-Security-Policy "upgrade-insecure-requests" always;
Browsers then request http:// subresources over HTTPS. This hides the symptom without correcting the generated URLs, and it fails for resources on hosts that lack HTTPS.
10. Verify
curl -s https://example.com/checkout/ | grep -c "http://example.com"
Expected output is 0. Reload with DevTools open and the cache disabled, and confirm the Console shows no Mixed Content entries.
Why This Fails for Some Users
xmlns="http://www.w3.org/2000/svg"false positives: This attribute is an XML namespace identifier, not a network request. It never triggers mixed content and must not be replaced. Restrict searches tosrc,href,xlink:href,data, andurl().- Redirect loop after switching to Force HTTPS: Under Flexible mode, a plugin or
.htaccessrule that redirects HTTP to HTTPS at the origin loops forever, because every Cloudflare-to-origin request is HTTP. Fix theHTTPSdetection first (step 3) or move to Full (strict) before enabling any origin-side redirect. - Stale object cache: Redis or Memcached can keep the old
homeandsiteurlvalues after the database update. Runwp cache flushand restart the object cache service. WP_HOMEandWP_SITEURLconstants: These override the database options, sowp option updateappears to succeed while the site still emitshttp://. Checkwp-config.phpand anymu-pluginsthat define them.- Serialized data corruption: Running raw SQL
REPLACE()onwp_optionsorwp_postmetabreaks serialized string lengths. Usewp search-replaceor the Better Search Replace plugin, which handle serialization. - CDN or offload plugins: WP Offload Media, custom CDN URL settings, and image optimizers store their own base URL. If it starts with
http://or a protocol-less URL forced to HTTP, SVGs served from the CDN stay insecure. Update it in the plugin settings. - Custom SVG upload plugins: Safe SVG and similar plugins store attachment URLs generated at upload time. Re-save the media, or run the
search-replacefrom step 5 againstwp_postsandwp_postmeta. - Cloudflare Tunnel or Workers: The tunnel’s origin service is typically
http://localhost:PORT, so the origin sees HTTP even in Full mode. Apply step 3 in that case. A Worker or Transform Rule that strips or overwritesX-Forwarded-Protobreaks header detection. - Multisite and domain mapping: Each site has its own
homeandsiteurlinwp_{blog_id}_options. Run the replacements with--url=for each site or use--network. - WordPress 5.7+ auto-upgrade limits: WordPress rewrites insecure URLs in post content only when
homeis HTTPS. It does not touch URLs generated by plugin or theme code, or stored in postmeta and options. - HSTS and browser caching: A previously cached CSS file that contains
http://URLs keeps producing warnings until the browser cache is cleared or the file URL changes (version query string). Regenerate the file in step 7 and hard-reload. - CSS
url()in inline styles from WooCommerce Blocks: Block-editor-generated inline CSS is cached inwp_optionsand post content. Re-save the affected template under Appearance > Editor after fixing the URL detection.