Fix the SSL Mixed Content Warning (HTTPS Site Shows "Not Secure")

Your site is on HTTPS but the browser shows "Not fully secure." Diagnose which of 6 mixed-content cases you have and fix it for good — with current 2026 Chrome behavior.

TL;DR: Open DevTools Console, filter for Mixed Content, and read the exact http:// URLs it lists. If they point at your own domain, you have stale content in your CMS/database — run a database search-replace. If they point at a third party, swap the snippet for its HTTPS endpoint or drop it. As a fast backstop for passive resources (images/audio/video), add the response header Content-Security-Policy: upgrade-insecure-requests. Active resources (scripts, iframes, fetch, WebSockets) still need a real working HTTPS source — there is no shortcut for those.

You finished migrating to HTTPS. The cert is valid, the lock icon appears for a moment — and then turns into an “i” or a triangle with the message Not fully secure / Your connection to this site is not fully secure. Open DevTools and you’ll see lines like:

Mixed Content: The page at 'https://yourdomain.com/' was loaded over HTTPS,
but requested an insecure element 'http://cdn.example.com/banner.jpg'.
This content was loaded over an insecure connection. This request has been
automatically upgraded to HTTPS.

Or, when the upgrade can’t be completed, you’ll see it blocked instead:

Mixed Content: The page at 'https://yourdomain.com/' was loaded over HTTPS,
but requested an insecure script 'http://cdn.example.com/widget.js'.
This request has been blocked; the content must be served over HTTPS.

This is mixed content: an HTTPS page that pulls in subresources over plain HTTP. Modern browsers treat it as a security regression and act on it automatically — they no longer quietly show the insecure resource. Worth knowing for context: Chrome is finishing its move to HTTPS-by-default. As of June 2026, “Always Use Secure Connections” is already on for Enhanced-Safe-Browsing users (Chrome 147, April 2026) and becomes the default for everyone in Chrome 154 (October 2026), per Google’s HTTPS by default announcement. The pressure to eliminate every http:// subresource only goes up from here.

Passive vs active mixed content (browsers treat them differently)

The single most important distinction. It decides whether a resource silently upgrades or hard-breaks.

TypeExamplesCurrent Chrome behavior (as of June 2026)
Passive (upgradable)<img>, <audio>, <video>, <source>, CSS background-image over HTTPAuto-upgraded to HTTPS. If the HTTPS version loads, fine. If it does not exist, the resource is not loaded — it does NOT fall back to HTTP. Console logs the upgrade or the failure.
Active (blockable)<script>, <link rel="stylesheet">, <iframe>, fetch(), XMLHttpRequest, web fonts, WebSocketBlocked outright. The element never loads.
Any HTTP request to an IP-address hosthttp://93.184.215.14/x.pngBlocked, not upgraded — even for images. Use a hostname.

Two things changed from older advice you may have read. First, browsers no longer fall back to HTTP for passive content that fails to upgrade — per MDN’s Mixed content reference, the request is upgraded and, if HTTPS isn’t available, simply fails. Second, the legacy block-all-mixed-content CSP directive is deprecated; the modern lever is upgrade-insecure-requests.

This is why you sometimes see a “Not secure” warning with visibly broken behavior (active blocked, or a passive image that has no HTTPS twin) and sometimes the lock just downgrades to an “i” while the page looks fine.

How to identify which case you’re in

Case 1: Hardcoded http:// in your own templates

Older WordPress themes, hand-written HTML, or templates copy-pasted years ago have absolute http:// URLs in src= / href= / srcset=.

How to spot it:

# Inside your project source
grep -rn 'src="http://' src/ public/ templates/
grep -rn 'href="http://' src/ public/ templates/
grep -rn '"http://' src/ | grep -v 'http://localhost'

In the browser, DevTools → Network → filter “http” → look for non-localhost HTTP requests.

Fix: change http:// to https://. If you don’t know whether the target supports HTTPS, test with curl -I https://that-host.com/path first.

Case 2: Old CMS / database content references HTTP

Your WordPress, Ghost, or self-rolled CMS has thousands of posts with <img src="http://yourdomain.com/wp-content/uploads/..."> hardcoded into post bodies. The template is fine; the database content is not.

How to spot it: DevTools mixed content warnings reference your own domain over HTTP. URLs look like http://yourdomain.com/... not third-party.

Fix (WordPress):

# Use wp-cli, NOT a naive SQL UPDATE — serialized PHP data breaks
wp search-replace 'http://yourdomain.com' 'https://yourdomain.com' \
  --all-tables --skip-columns=guid --dry-run

# Remove --dry-run when satisfied

For other CMSes: run their migration / find-replace tool. Avoid raw SQL on tables containing serialized data (it corrupts length prefixes).

Case 3: Third-party widget / embed only available over HTTP

An analytics script, social-share button, chat widget, or ad slot from a legacy provider that never added HTTPS support. The provider’s HTTPS endpoint either doesn’t exist or returns a cert error.

How to spot it: DevTools shows the mixed-content URL is on a third-party domain. curl -I https://that-provider.com/widget.js either fails or returns SSL errors.

Fix: (a) ask the provider for an HTTPS endpoint — most have one but you’re using a stale snippet; (b) self-host the widget by proxying through your own HTTPS; (c) drop the widget.

Case 4: Protocol-relative URLs (//cdn...) resolving to HTTP in dev

Older code used //cdn.example.com/lib.js (no protocol), which means “match the page’s protocol.” On HTTPS pages it loads via HTTPS; on http://localhost dev it loads via HTTP. Works in prod, breaks in dev, gets misdiagnosed.

How to spot it: warnings appear only on localhost / staging over HTTP, never on prod.

Fix: switch to explicit https://. Protocol-relative URLs were deprecated by the W3C in 2014 because they encourage HTTP fallback paths.

Case 5: Resources loaded by inline JavaScript / fetch / WebSocket

fetch('http://api.example.com/data') or new WebSocket('ws://...') from an HTTPS page. Active mixed content — blocked. The error often appears as a fetch failure in the console, not labeled “mixed content.”

How to spot it: a fetch / XHR rejection in DevTools Network with status (blocked:mixed-content). WebSocket connections to ws:// from HTTPS pages fail with “An insecure WebSocket connection may not be initiated.”

Fix: change http:// to https:// and ws:// to wss:// in your JavaScript. If the API doesn’t support HTTPS, you can’t fix this client-side — you must proxy through your own HTTPS endpoint.

Case 6: <iframe> from HTTP source

Iframes are active content. An <iframe src="http://..."> on an HTTPS page is blocked.

How to spot it: the iframe area is blank or shows a browser error. DevTools console: Mixed Content ... iframe ... was blocked.

Fix: switch to HTTPS source. Some old embed providers (legacy maps, archive.org pre-2017) had HTTP-only embeds — find a newer endpoint or self-host.

Shortest fix path

In hit-rate order:

  1. DevTools → Console → filter “Mixed Content”: get the exact list of offending URLs.
  2. Group them: your domain over HTTP (CMS / DB) vs your templates (find-replace in source) vs third-party (per-provider research).
  3. For your-domain HTTP: run a database search-replace.
  4. For template hardcodes: grep source, replace, commit, redeploy.
  5. For third-party: check if HTTPS endpoint exists; if not, drop or proxy.
  6. As a backstop, add Content-Security-Policy: upgrade-insecure-requests to your HTTP response headers — this tells the browser to auto-rewrite http:// to https:// for all subresources. Covers passive content silently; active content still needs HTTPS to actually work.
# nginx
add_header Content-Security-Policy "upgrade-insecure-requests" always;
<!-- HTML meta as a fallback if you can't set headers -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
  1. Verify with the checks in the next section.

How to confirm it’s fixed

Don’t trust a single page. Mixed content hides on the one post nobody opens.

# Option A: command-line scan of a single page
curl -s https://yourdomain.com/ | grep -oE 'http://[^"'"'"' ]+' | sort -u

# Option B: dedicated crawler (recommended — checks many pages)
# https://www.jitbit.com/sslcheck/   recursively crawls and lists insecure scripts/images
# https://www.whynopadlock.com/       single-page deep check

For an at-scale, ongoing report, run a Content-Security-Policy in report-only mode so the browser tells you about violations without breaking anything:

# Logs every mixed-content (and other) violation to your endpoint; blocks nothing
add_header Content-Security-Policy-Report-Only
  "upgrade-insecure-requests; report-to csp-endpoint" always;

Final manual check: hard-refresh the page in an incognito window with DevTools open. The Console filtered to Mixed Content should be empty, and the address-bar lock should be solid (no “i” / triangle). For sites with many pages, a CI job that fails the build when grep -rE 'http://[^"]' dist/ returns hits (excluding comments and http://localhost) is the only reliable long-term safeguard.

Prevention

  • Use protocol-absolute https:// URLs in all new content. Protocol-relative // is deprecated and creates dev/prod inconsistency.
  • Set Content-Security-Policy: upgrade-insecure-requests in your global response headers — it’s a one-line defense against passive mixed content slipping back in.
  • Add a CI check that scans built HTML for http:// outside of comments. Fail the build on a hit.
  • For WordPress / Ghost / CMS sites: configure the canonical site URL once at https:// and validate every theme/plugin update doesn’t regress (some emit hardcoded HTTP).
  • Audit third-party embeds yearly: providers occasionally drop HTTPS or change endpoints. Stale embed snippets are the most common regression source.

FAQ

Q: Will mixed content hurt my Google ranking? A: Not directly, but indirectly yes. HTTPS has been a ranking signal since 2014. A page that loads over HTTPS but shows a “Not secure” warning loses user trust, and the engagement signals that follow (bounce rate, time on page) suffer. And a passive image with no HTTPS twin now simply doesn’t render — that broken hero or product shot hurts conversion directly.

Q: Does upgrade-insecure-requests fix everything? A: It tells the browser to rewrite every http:// subresource to https:// before requesting it, for both passive and active content. The catch: the upgrade only works if the target actually serves that resource over HTTPS. If it doesn’t, the upgraded request fails and the element doesn’t load. So you still need every target host to support HTTPS — upgrade-insecure-requests just removes the need to rewrite each URL by hand. It does not let an HTTP-only host magically work.

Q: Chrome 154 / “Always Use Secure Connections” is now on. Does that change anything for my mixed content? A: That setting governs the top-level navigation (typing or clicking a bare http://yourdomain.com shows a warning before loading). Mixed content is about subresources on a page that’s already HTTPS — a separate mechanism that has been enforced for years. Fixing your http:// subresources, as in this article, is what clears the in-page “Not secure” state regardless of the navigation setting.

Q: My WordPress site still shows mixed content after I changed the site URL. A: The site URL only controls new content. Existing posts have hardcoded http:// in their content. Run wp search-replace 'http://yourdomain.com' 'https://yourdomain.com' --skip-columns=guid to fix the database. Don’t skip --skip-columns=guid or you’ll break old feed readers.

Q: I see mixed-content warnings only for one specific page. Why? A: That page references something the rest of the site doesn’t — usually a banner image, a custom embed, or a one-off third-party script. Open DevTools on that exact page and check the Network tab for HTTP requests.

Q: A third-party widget I depend on is HTTP-only and the vendor refuses to add HTTPS. What now? A: Proxy it through your own server. Set up a route at https://yourdomain.com/widget-proxy/ that server-side-fetches the HTTP resource and serves it over your HTTPS. Adds latency and bandwidth cost, but it’s the only way short of dropping the widget.

Tags: #Domain #DNS #SSL #Troubleshooting #SSL mixed content