Resolving mixed content HTTP warnings on WooCommerce SVG icons behind Cloudflare SSL

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).

See also  Fixing duplicate slug append (-2) on published WordPress pages with empty trash

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
See also  WooCommerce Webhook Disabled Automatically Due to Delivery Failure: How to Reset

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 to src, href, xlink:href, data, and url().
  • Redirect loop after switching to Force HTTPS: Under Flexible mode, a plugin or .htaccess rule that redirects HTTP to HTTPS at the origin loops forever, because every Cloudflare-to-origin request is HTTP. Fix the HTTPS detection first (step 3) or move to Full (strict) before enabling any origin-side redirect.
  • Stale object cache: Redis or Memcached can keep the old home and siteurl values after the database update. Run wp cache flush and restart the object cache service.
  • WP_HOME and WP_SITEURL constants: These override the database options, so wp option update appears to succeed while the site still emits http://. Check wp-config.php and any mu-plugins that define them.
  • Serialized data corruption: Running raw SQL REPLACE() on wp_options or wp_postmeta breaks serialized string lengths. Use wp search-replace or 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-replace from step 5 against wp_posts and wp_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 overwrites X-Forwarded-Proto breaks header detection.
  • Multisite and domain mapping: Each site has its own home and siteurl in wp_{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 home is 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 in wp_options and post content. Re-save the affected template under Appearance > Editor after fixing the URL detection.
See also  WordPress site health error: Background updates not working due to version control checkout

Leave a Comment