BlogMixed Content and SSL Gaps: The Padlock That Isn'tScan · Site Quality

Mixed Content and SSL Gaps: The Padlock That Isn't

OH
Omar Haddad · October 2025 · 9 min read

TL;DR

You bought the certificate, you serve over HTTPS, the padlock should be solid, and then one http:// image, script, or font on an otherwise secure page breaks it. That is mixed content, and since Chrome 79 browsers have stopped tolerating it: passive resources get auto-upgraded, and active ones (scripts, stylesheets, iframes) are blocked outright, which can silently break your page. Worse, mixed content reintroduces the exact interception risk HTTPS exists to prevent, an attacker on the network can tamper with the insecure resource. At checkout, a "Not Secure" warning or a broken padlock is a direct conversion killer. A scan finds every offending URL; you cannot eyeball them.

The padlock is a promise you can accidentally break

HTTPS makes a specific promise: everything on this page traveled over an encrypted, authenticated connection that a network attacker cannot read or tamper with. The padlock is the browser vouching for that promise. Mixed content breaks it in a precise way, the main HTML arrived securely over HTTPS, but the page then asks the browser to load a subresource (an image, a script, a stylesheet, a font, an iframe) over plain HTTP. Now part of the page is unprotected, the promise is void, and the browser has to react.

The reaction depends on what kind of resource it is, and this is where many developers' mental model is out of date. Per the MDN reference on mixed content, browsers split mixed content into two categories with very different handling, and in 2025 that handling is much stricter than it used to be. The era when an HTTP image on an HTTPS page just produced a quiet console warning is long gone.

Active vs. passive, the distinction that decides severity: passive mixed content (images, audio, video) cannot alter the rest of the page, so it is the lesser risk. Active mixed content (scripts, stylesheets, iframes, fetch/XHR) can read and rewrite the entire page, so a tampered version means full page compromise. Browsers now block active mixed content by default, which means the "fix" the browser applies is to silently remove the resource, breaking whatever depended on it.

What browsers do in 2025

The tolerant era is over. Per The SSL Store's guide to finding and fixing mixed content, modern Chrome handling, established since Chrome 79 began blocking mixed content by default, is roughly:

  • Passive mixed content is automatically upgraded from http:// to https:// where possible. If the secure version exists, the user never notices. If it does not, the resource may be blocked, and your image or video simply does not appear.
  • Active mixed content is blocked outright. A stylesheet loaded over HTTP does not apply; a script does not run; an iframe does not render. The page does not warn the user gently, it silently omits the resource, which can break layout or functionality with no obvious cause.
  • The security indicator degrades. Depending on the case, the user may see a "Not Secure" label or a downgraded padlock, and that is the part your conversion rate feels. Browsers also mark sites using outdated TLS 1.0 or 1.1 as "Not Secure, " so a TLS misconfiguration produces the same warning as mixed content.

The dangerous outcome is the silent one. A developer tests on a page where the auto-upgrade happened to work, ships, and a different page with an active mixed-content script breaks for real users with no error in the developer's own session. The bug is environmental and intermittent in exactly the way that makes it hard to catch by hand, it depends on which resources exist over HTTPS, which the developer cannot tell by looking at the page.

The trust cost at the worst possible moment

Mixed content is a security problem, but its business impact is a trust problem, and it lands hardest where trust matters most: checkout. A user about to enter a credit card glances at the address bar, the place they have been trained to look, and sees "Not Secure" or a broken padlock instead of the reassuring lock. Whatever the technical cause, the message they receive is "do not put your card number here." Per SecurityScorecard's analysis of HTTPS misconfiguration risks, these warnings directly erode user confidence, and at the payment step, eroded confidence is an abandoned cart. The cost is not theoretical and it is not small: it is the conversion you lose on the highest-intent page you have.

The actual security risk is real too. The whole point of HTTPS is to stop a network attacker, on public wifi, a compromised router, a hostile ISP, from reading or modifying traffic. An active mixed-content script delivered over HTTP can be swapped out by that attacker for malicious code that runs in the context of your secure page, defeating the encryption you paid for and potentially stealing the very card numbers the padlock was supposed to protect. Mixed content is not cosmetic; it is a hole in the boat that happens to also scare off the passengers.

How mixed content sneaks in

Almost nobody types http:// on purpose anymore. Mixed content arrives through legacy and inattention.

Hardcoded HTTP URLs in old content

CMS posts, email templates, and product descriptions written years ago contain absolute http:// image and embed URLs. They were fine before the migration to HTTPS; now every one is a mixed-content trigger baked into your content, scattered across thousands of database rows nobody is going to read.

Third-party widgets and embeds

An older widget, ad tag, map, or social embed that loads its own resources over HTTP brings mixed content along for the ride. You loaded the widget securely; it loaded an HTTP script. You do not control its source, so you cannot fix it by editing your own code.

Hardcoded config and CDN URLs

A configuration value, a hardcoded asset host, or a CDN reference left on http:// propagates to every page that uses it, turning a single stale config line into a site-wide problem.

Find every offender, you cannot eyeball this

The defining feature of mixed content is that it is invisible until something breaks, and it can hide on any of thousands of pages and inside third-party resources. The only reliable detection is to load pages and inspect every subresource request for an insecure scheme. Browsers report blocked mixed content to the console and via CSP reporting, which you can collect at scale.

// Crawl pages and flag any subresource requested over http://
const page = await browser.newPage();
const mixed = [];
page.on('request', (r) => {
  const u = new URL(r.url());
  if (u.protocol === 'http:' && r.frame().url().startsWith('https:')) {
    mixed.push({page: r.frame().url(), resource: r.url(), type: r.resourceType()});
  }
});
await page.goto(targetUrl, {waitUntil: 'networkidle'});

// Active types are the urgent ones, they get BLOCKED and break the page
const active = mixed.filter((m) =>
  ['script', 'stylesheet', 'xhr', 'fetch', 'document'].includes(m.type));
console.log('Active (blocked, breaks page):', active);
console.log('Passive (upgraded or missing):', mixed.length - active.length);

You can also have the browser itself report violations by adding a CSP that both fixes and surveils mixed content. The reports tell you exactly which resource on which page is the problem, in production, from real navigations, which catches the long-tail pages a crawl might miss and the third-party resources that only load under certain conditions.

Fix and enforce in one move

The cleanest fix is a single CSP directive that converts the whole problem into a non-issue: upgrade-insecure-requests tells the browser to automatically rewrite every http:// subresource on the page to https:// before requesting it. Combined with HSTS so the connection can never downgrade in the first place, you both remediate existing mixed content and prevent new instances from breaking anything.

# Fix existing mixed content AND prevent new instances, at the header level
add_header Content-Security-Policy "upgrade-insecure-requests" always;

# Force HTTPS so the connection can never start insecure
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

# Optionally, surveil what would have been mixed content via report-to:
# Content-Security-Policy-Report-Only: ...; report-uri /csp-report

The header is the safety net, not the excuse to skip the real cleanup. Fix the source URLs too, update the hardcoded http:// references in your content and config, ideally with a one-time database find-and-replace from http://yourdomain to https://yourdomain and protocol-relative or HTTPS URLs for assets. Why both? Because upgrade-insecure-requests only works if the HTTPS version of each resource actually exists. An HTTP-only third-party resource still breaks after the upgrade attempt; for those you must find a secure source or remove the dependency entirely.

Insight: the layered fix is HSTS (connection can never downgrade) + upgrade-insecure-requests (subresources auto-rewritten to HTTPS) + cleaning the source URLs (so the HTTPS versions actually load) + a current TLS configuration (so the certificate itself never triggers the warning). Each layer covers a different gap; together they make the "Not Secure" warning structurally impossible rather than something you chase page by page.

Don't forget the certificate itself

Mixed content is one way to break the padlock; a bad certificate is the other, and it produces the same scary warning. An expired certificate, a certificate that does not cover the exact hostname (a bare-domain cert on a www URL), a missing intermediate certificate in the chain, or support for deprecated TLS versions all trigger browser warnings that cost you the same conversions. The same scan that hunts mixed content should validate the certificate's expiry, hostname coverage, chain completeness, and TLS version, because from the buyer's perspective, "Not Secure" is "Not Secure" regardless of which underlying cause produced it.

Keep the padlock solid

Mixed content regresses the moment someone pastes an old http:// embed into a CMS post, adds a legacy widget, or restores a hardcoded asset URL. Certificates expire on a schedule whether or not anyone is watching the renewal. Make detection continuous: crawl for insecure subrequests on a schedule and on deploy, collect CSP violation reports from production, monitor certificate expiry well ahead of the deadline, and treat any active mixed content or certificate problem as a release-blocking, page-blocking defect. The padlock is only as solid as your least-maintained page and your soonest-to-expire certificate.

The bottom line

Mixed content is the padlock that isn't: a single http:// resource on an HTTPS page breaks the security promise, and in 2025 browsers respond by blocking active resources outright, silently breaking your page, and degrading the security indicator users check before they trust you with a card. It reintroduces the exact network-tampering risk HTTPS exists to stop, and its conversion cost peaks at checkout, the worst possible place for a "Not Secure" warning. Find every insecure subresource with a crawl and CSP reporting, fix it with upgrade-insecure-requests plus HSTS plus cleaning the source URLs, validate the certificate and TLS configuration in the same pass, and re-scan continuously so a pasted-in legacy embed or a lapsed renewal cannot reopen the hole. The padlock is a promise, a scan keeps it one you can actually keep.

Find the Gaps Before They Cost You

Scan audits your site for the accessibility, performance, AEO, and security gaps that quietly drain revenue and invite lawsuits, in one pass.

Try Scan Free →
Omar Haddad Omar Haddad writes about AI quality engineering at alt.qa, built by TheWorkCompany.