Mixed content: the http:// leftovers inside your https:// page
Why one forgotten http:// image or script undoes the security of an HTTPS page, what browsers block or quietly upgrade, and how to clean it up for good.
Security · Published: 25 September 2026 · 7 min read · Дмитрий Орлов
Moving a site to HTTPS takes an afternoon: get a certificate, redirect port 80, done. Then the padlock refuses to show up on half the pages, a slider stops working, or a font silently falls back to Arial. The page itself arrives over https://, but something inside it still asks for http://. That is mixed content.
Why one http:// request matters
HTTPS protects the whole page only if every byte of it comes over an encrypted connection. A single script loaded over plain HTTP can be read and replaced by anyone on the path — public Wi-Fi, a compromised router, an injecting ISP. A replaced script runs with full access to the page: it can read form fields, rewrite links and steal session data. The encryption of the HTML document no longer means anything.
Browsers split mixed content into two groups by how much damage it can do.
- Active (blockable) content: scripts, stylesheets, iframes, fonts,
fetchand XHR requests, workers. Anything that can change the page or read from it. Modern browsers block it outright — the request is never sent, and the console logs an error. - Passive (upgradable) content: images, audio and video. The worst an attacker can do is swap a picture. Current Chrome and Firefox no longer load these over HTTP at all: they rewrite the URL to https:// automatically, and if the resource is not available over HTTPS, it simply fails to load.
Older browsers and some embedded web views still display passive mixed content as is, and mark the page as not fully secure — that is where the missing or crossed-out padlock comes from.
Forms are a separate case. A <form action="http://..."> on an HTTPS page sends the user's input unencrypted. Browsers warn before submitting such a form, and the warning looks alarming enough that many people abandon it.
Where it comes from
Almost every mixed-content issue is a leftover from the time before the migration.
- Hardcoded URLs in the CMS database. Editors inserted images while the site was on HTTP, so post bodies, widgets and settings still contain
http://example.com/uploads/.... Changing the site address in the admin panel does not touch content already saved. - Old embeds. Video players, maps, counters and chat widgets pasted years ago with an http:// URL. Some of those providers now serve HTTPS; some no longer exist.
- CSS.
background-image: url(http://...)in a stylesheet or an inlinestyleattribute, and@font-facepointing to an HTTP font host. - Responsive images. The
srcwas fixed, butsrcsetstill lists http:// candidates, and the browser may pick one of those on a high-density screen. - Templates and JavaScript. A theme that builds absolute URLs from a hardcoded scheme, or a script that injects
http://resources at runtime.
How to find it
DevTools. Open the page, open the console, reload. Every mixed-content request is reported with the exact URL and whether it was blocked or upgraded. In the Network panel of Chrome the filter mixed-content:all narrows the list to these requests. This works for one page at a time.
Static HTML. A quick first pass over the markup:
curl -s https://example.com/ | grep -Eo '(src|href|srcset|action)="http://[^"]+"'
It will not see URLs in CSS files or those added by JavaScript, but it catches most of the CMS leftovers.
CSP reports. To find mixed content across the whole site with real traffic, send a report-only policy that allows only HTTPS sources:
Content-Security-Policy-Report-Only: default-src https: data: blob: 'unsafe-inline' 'unsafe-eval'; report-uri /csp-report
Nothing gets blocked; every http:// load produces a report with the page and the URL. Note that upgrade-insecure-requests has no effect in a report-only policy, and the old block-all-mixed-content directive is deprecated — modern browsers already block active content by default.
A SiteScan report also checks this: it flags any scanned page that loads assets over http:// under "Serve every asset over https://".
How to fix it
Use https:// or relative URLs. For your own assets, a path like /uploads/photo.jpg is the most robust option: it follows the scheme and the domain of the page. For third-party resources, switch to https:// and check that the provider actually serves it. If they do not, replace or self-host the resource.
Search and replace in the database — carefully. Back up first. In WordPress, use WP-CLI rather than raw SQL, because it handles serialized PHP data correctly:
wp search-replace 'http://example.com' 'https://example.com' --all-tables --dry-run
Run it without --dry-run once the counts look right. A plain SQL REPLACE changes string lengths inside serialized values and can corrupt widget and plugin settings.
Add upgrade-insecure-requests as a safety net. With this header the browser rewrites every http:// subresource on the page to https:// before requesting it:
Content-Security-Policy: upgrade-insecure-requests
It is a safety net, not a fix: if a host has no HTTPS, the request just fails, and the http:// URLs remain in your content and in your feeds.
Turn on HSTS once the site is clean. Strict-Transport-Security: max-age=31536000; includeSubDomains tells the browser to never contact your domain over HTTP again. Start with a short max-age and raise it once you are sure every subdomain works over HTTPS.
Check-list
- The console shows no mixed-content warnings on the main templates.
- No http:// URLs in
src,href,srcset,actionor CSSurl(). - Old URLs in the database are replaced with a serialization-safe tool.
- Third-party embeds load over HTTPS or are removed.
upgrade-insecure-requestsis set as a safety net.- HSTS is enabled after everything above is clean.
Read next
Check how much of this is on your own site
A free scan surfaces broken links, JS errors and performance problems in about a minute.