Bali, Indonesia
Insights

Fix Mixed Content in the Database or Rely on upgrade-insecure-requests?

October 24, 2026
0:00
Your SSL certificate is installed and valid, and the site redirects to HTTPS. The browser still won't show a clean padlock. Some images are missing, or a slider stopped working. The developer console says the page "was loaded over HTTPS, but requested an insecure" resource. The usual question is whether to fix the URLs in the database or add a header or plugin that makes the warning go away. Both approaches can work, but they solve different problems. The header and plugin route changes what the browser fetches. The database route changes what WordPress actually stores and outputs. To choose, find out where the insecure URLs come from first.
Mixed content means an HTTPS page is pulling in subresources over plain http://. Subresources are the images, scripts, stylesheets, fonts, iframes, and AJAX calls that a page loads after the HTML. The certificate is fine. The page is just asking for some of its pieces over an unencrypted connection. Browsers sort mixed content into two groups, and they treat the groups differently:
  • Passive content: images, audio, and video. These can't change the rest of the page. Modern browsers typically try to upgrade them to HTTPS automatically, and they show a warning or drop the resource if that fails.
  • Active content: scripts, stylesheets, iframes, and fetch or XHR requests. These can rewrite the page, so browsers block them outright.
This split explains a common confusing symptom. The site looks mostly fine but the menu or contact form is dead, because a single JavaScript file got blocked. Open DevTools, reload with the cache disabled, and read the Console. Every mixed content line names the exact URL that failed. Write a few of those URLs down. They are your trail.
The URL in the console tells you what failed, not who produced it. In WordPress, an insecure URL usually comes from one of five places:
  1. The home and siteurl options still say http://. In that case almost every URL WordPress generates is wrong.
  2. Post and page content. The editor saves absolute image URLs into post_content, so every image inserted before the move still points at http://.
  3. Options and postmeta. Theme customizer settings, widgets, and page builder data often store absolute URLs, frequently as serialized PHP arrays.
  4. Theme or plugin files. Hardcoded http:// in a template, or a url(http://...) inside a CSS file.
  5. Third-party resources. An embed, a font, or a tracking script from another domain.
One case looks like a database problem but isn't: WordPress may not know it's running on HTTPS. If SSL terminates at a CDN, load balancer, or reverse proxy, PHP receives the request over plain HTTP. WordPress then decides is_ssl() is false and generates http:// URLs even when the database is correct. This shows up often after a move behind a proxy, and on the support desk it's one of the first things worth ruling out. A search-replace won't help here. Start with read-only checks. WP-CLI makes this quick, and --dry-run reports matches per table without writing anything:
BASH
# What WordPress thinks its own address is
wp option get home
wp option get siteurl

# Count stored http:// references to your own domain, without changing anything
wp search-replace 'http://example.com' 'https://example.com' --all-tables-with-prefix --dry-run

# Hardcoded http:// in the active theme's PHP and CSS files
grep -rn 'http://' wp-content/themes/your-theme --include='*.php' --include='*.css'
To read the results:
  • If home and siteurl are already https://, the dry run finds almost nothing, and the page source is still full of http:// links to your own domain, suspect HTTPS detection. Confirm by checking whether the generated URLs change depending on how you reach the site, for example directly on the origin versus through the CDN.
  • If the dry run reports matches in wp_posts and wp_postmeta, the source is stored content.
  • If the grep finds hits, the theme is involved.
  • If the console URLs belong to someone else's domain, none of the above applies. Either that host supports HTTPS and you change the embed, or it doesn't and the resource has to go.
Watch out for page caching while you test. A full-page cache or CDN can keep serving old HTML with http:// links for a while after you've fixed the source. If you clear your browser cache but not the server cache, a correct fix will look like it failed.
When the dry run confirms stored URLs, a database search-replace is the durable fix. It changes the data itself, so every consumer gets correct URLs. That includes browsers, RSS readers, the REST API, social media link previews, and emails sent by the site. Use a tool that understands serialized data. WordPress stores many settings as serialized PHP strings, and those strings record their own length. A raw SQL UPDATE ... REPLACE() changes http to https, the string becomes one character longer, the recorded length no longer matches, and PHP can't read the value anymore. The visible result is widgets, theme settings, or page builder layouts that quietly reset. WP-CLI's search-replace unserializes the data, replaces the text, and serializes it again with correct lengths. Take a database export before writing anything. This is the one step in the process that is hard to undo without a backup:
BASH
# 1. Back up first
wp db export before-https-replace.sql

# 2. Preview again, skipping the guid column
wp search-replace 'http://example.com' 'https://example.com' --all-tables-with-prefix --skip-columns=guid --dry-run

# 3. Run for real only after the preview looks right
wp search-replace 'http://example.com' 'https://example.com' --all-tables-with-prefix --skip-columns=guid

# 4. Clear the object cache so stale values are not served
wp cache flush
The guid column is skipped by convention. It acts as a permanent identifier for posts, especially in feeds, and isn't meant to be rewritten as a link. Include the www or non-www variant of your domain if both were ever used, and run a separate pass for each. After the replace, purge the page cache and the CDN cache too. For theme files, fix the code directly. In templates, use WordPress functions such as home_url() or get_template_directory_uri() instead of hardcoded addresses. In CSS, use HTTPS or relative paths. If it's a third-party theme, a theme update or a child theme override keeps your change from being overwritten later. For the proxy case, tell WordPress the original request was HTTPS. A common approach is a short block in wp-config.php, above the line that loads wp-settings.php. It checks the forwarded protocol header and sets $_SERVER['HTTPS'] = 'on' when the value is https. The header name depends on your proxy or CDN. Only trust it if your proxy actually sets it and clients can't reach the origin directly, because anyone can send that header themselves.
upgrade-insecure-requests is a Content-Security-Policy directive. It tells the browser to rewrite http:// subresource URLs to https:// before fetching them. It's a native browser feature with no PHP involved and no output rewriting, which makes it a useful safety net.
NGINX
# Inside the HTTPS server block
add_header Content-Security-Policy 'upgrade-insecure-requests' always;
On Apache, the equivalent in .htaccess or the vhost is Header always set Content-Security-Policy 'upgrade-insecure-requests', which needs mod_headers enabled. In Nginx, if a location block defines its own add_header directives, it stops inheriting the server-level ones, so the header can disappear on some paths. Check with curl -sI https://example.com/ and look for the header in the response. Its limits matter more than its convenience:
  • It doesn't fall back. If the HTTPS version of a resource doesn't exist, the resource just fails. A third-party host without HTTPS stays broken, only now the failure is silent.
  • It only affects browsers that honor the policy. Feeds, API consumers, emails, and link preview scrapers still receive http:// URLs.
  • It's ignored in a Content-Security-Policy-Report-Only header, so you can't trial it in report-only mode.
  • It hides the data problem. The database stays dirty, and if the header disappears later during a server move or config rewrite, the mixed content comes back.
SSL helper plugins usually work in one of two ways. Some send this same header. Others buffer the page output and rewrite http:// to https:// before it's sent. The output rewrite costs some processing on every uncached request. Like the header, it only fixes what goes out through that one path. A typical support pattern: the padlock turns clean after a plugin is installed, then breaks again months later when the plugin is deactivated during troubleshooting, and nobody remembers why it was there.
Where the http:// URL comes fromDurable fixIs the header or plugin enough?
home / siteurl optionsUpdate both to https://No
Post content, postmeta, optionsWP-CLI search-replace after a backupOnly as a temporary bridge
Theme or plugin filesFix the code or update the themeAs a stopgap
WordPress not detecting HTTPS behind a proxyPass the forwarded protocol in wp-config.phpNo, URLs keep being generated wrong
Third-party resource without HTTPSReplace or remove itNo, the upgrade just fails
To verify, reload a few representative pages with DevTools open and the cache disabled. Check the home page, a blog post with older images, a page built with your page builder, and any form or checkout page. The Console should show no mixed content lines, and the Security panel should report a secure connection. Then view the raw HTML source and search it for http://. A clean console with http:// still in the source means the header is covering for you, not that the source is fixed. The practical rule: fix the source first with a backed-up WP-CLI search-replace, theme fixes, or correct proxy detection. Then add upgrade-insecure-requests if you want a safety net for stray URLs you didn't catch. Treat the header and plugins as a backstop, not as the fix, so the padlock doesn't depend on one header line or one plugin staying in place.