Handling Mixed Content After a Protocol Change

Problem Statement

A page served over HTTPS that references a stylesheet, script, image or iframe over HTTP is mixed content, and browsers treat the two categories very differently. Active mixed content — scripts, stylesheets, iframes — is blocked outright, which means a page that looks correct in your editor renders unstyled or non-functional for visitors with no console open. Passive mixed content — images, video, audio — usually loads but strips the padlock and downgrades the security indicator. Migrations produce mixed content in bulk because absolute URLs are baked into content, themes and third-party embeds, and none of them updates when the protocol or hostname changes. This page sits under TLS & Certificate Cutover and covers finding those references before visitors do.

When to Use This Approach

  • The migration moves the site from HTTP to HTTPS, or changes the hostname assets are served from.
  • Content is authored in a system that stores absolute URLs in the database.
  • Third-party embeds — maps, video players, chat widgets, ad tags — appear on the site.
  • The new origin serves correctly in isolation but pages render partially broken.
  • You need a list of offending URLs rather than an impression that “some pages look odd”.

Step-by-Step Instructions

1. Find the References in the Source, Not the Browser

The console tells you about the page you are looking at; a crawl tells you about the estate. Search the rendered HTML of every page in the inventory for http:// occurrences in attributes that load a subresource, and treat the resulting list as the work queue. Crawling is what turns this from an anecdote into a finite task.

# Grep rendered pages for insecure subresource references
grep -roE '(src|href)="http://[^"]+"' ./crawl-output/ \
  | sed 's/.*="//;s/"$//' | sort -u > mixed-content.txt
wc -l mixed-content.txt

Crawl the rendered output rather than the source templates, because a great many insecure references only appear once content has been interpolated into a page. A template containing `` is clean by inspection and emits an http:// URL for every article whose stored value predates the protocol change — which is exactly the population you are trying to find.

2. Separate Active From Passive

The two categories have different urgency and different remedies. Active references break functionality and must be fixed before launch; passive ones degrade the indicator and can occasionally be tolerated for a short period. Sorting the list by the element type that references each URL tells you which queue an item belongs in.

# Active first: scripts, stylesheets and frames are blocked outright
grep -E '\.(js|css)$|<iframe' mixed-content.txt > mixed-active.txt
grep -vE '\.(js|css)$|<iframe' mixed-content.txt > mixed-passive.txt

Sort each queue by page count rather than by URL, because mixed content is heavily clustered. A single stylesheet reference in a shared template accounts for thousands of affected pages while a thousand distinct image URLs each affect one, so fixing in descending order of blast radius clears most of the estate in the first few changes.

3. Fix at the Source, Not With a Rewrite Rule

Rewriting http:// to https:// in the response is tempting and treats the symptom: the underlying content still contains insecure URLs, so the problem returns on any page the rewrite does not cover and on every future export of that content. Update the stored values instead, using a tool that understands serialised data rather than a plain text substitution, which corrupts length-prefixed fields.

# WordPress example: rewrite stored URLs safely, dry-run first
wp search-replace 'http://www.example.com' 'https://www.example.com' \
  --dry-run --precise --skip-columns=guid

Take a backup immediately before running any bulk substitution, and verify it restores. A search-and-replace across a content database is among the least reversible operations in a migration — it touches every row it matches, in place, with no undo — and the moment to discover that the backup was incomplete is not after the substitution has run with an over-broad pattern.

4. Add a Reporting Policy Before You Need It

A Content-Security-Policy in report-only mode turns mixed content from something you have to go looking for into something that reports itself. Deploy it before the window so the reports describe the legacy estate, and keep it through the soak so anything introduced by the migration surfaces immediately.

# Report violations without blocking anything, during and after the window
add_header Content-Security-Policy-Report-Only
  "default-src https:; report-uri /csp-report" always;
Active versus passive mixed content Subresource types split by whether browsers block them outright or load them while downgrading the security indicator, with the user-visible result of each. One category breaks the page; the other breaks the padlock Reference Browser does Visitor sees script, stylesheet, iframe blocks it unstyled or broken page XHR / fetch blocks it features silently fail image, video, audio loads it no padlock link href to a page nothing — not a subresource nothing The bottom row is worth knowing: an http:// link to another page is not mixed content and does not need fixing for this reason.
Fix the top two rows before launch; the third can follow within the soak window if the volume is large.

Worked Example

A publisher moves to HTTPS as part of a hosting migration. The templates are updated and the site looks correct on the home page and on every article the team checks. A crawl tells a different story: 4,100 pages carry at least one insecure reference, almost all of them images embedded in article bodies with absolute http:// URLs written by editors over a decade.

$ wc -l mixed-content.txt
4118 mixed-content.txt
$ grep -c '\.js$\|\.css$' mixed-content.txt
7

The seven active references are the urgent ones — two analytics scripts and five stylesheet includes from an old theme — and are fixed before launch. The images are passive, so pages still render, but every affected article loses its padlock. A wp search-replace with --precise corrects the stored URLs in one pass, and a follow-up crawl returns zero. The report-only policy stays in place, and two weeks later it catches a newly published article whose author pasted an http:// image URL from an old document.

Where absolute insecure URLs hide Five places a migration finds hard-coded insecure URLs — article bodies, theme files, serialised settings, feeds and third-party embeds — with the tool that reaches each. Five hiding places, and a text search reaches only two Location Reached by Risk if missed Article bodies in the database search-replace tool padlock lost site-wide Theme and template files repository grep blocked scripts Serialised settings blobs serialisation-aware tool only corrupted on plain sed Feeds and API output crawl the endpoints too wrong URLs downstream Third-party embed snippets vendor documentation blocked iframes The middle row is the one that punishes a naive fix: a plain text substitution breaks the length prefix and destroys the field.
Use a tool that understands the storage format, and dry-run it before touching production.

Re-crawl after the fix rather than spot-checking the pages you know about. The substitution will have missed something — a table it did not cover, a field type it skipped, a subdomain outside its scope — and the only reliable way to find what remains is the same crawl that produced the original list, run again and diffed against it.

Verification

Confirm the estate is clean and stays clean.

# 1. No insecure subresource references remain in the crawl
grep -rcoE '(src|href)="http://' ./crawl-output/ | grep -v ':0' | wc -l

# 2. A representative page reports no blocked resources
curl -s https://www.example.com/article/example | grep -c 'src="http://'

# 3. CSP report endpoint is receiving and logging violations
curl -sI https://www.example.com/ | grep -i content-security-policy

A clean result is zero files with insecure references, zero occurrences on sampled pages, and a report-only policy present in the response headers.

Order of work for clearing mixed content Crawling to build the list, separating active from passive references, fixing stored content at source, and leaving a report-only policy in place to catch regressions. Crawl, triage, fix at source, then keep watching Crawl build the full list Triage active vs passive Fix at source stored content CSP report-only catches regressions Rewriting responses instead of content moves the problem rather than removing it — the insecure URLs survive in every export and feed. The final step is what stops the problem returning the next time somebody pastes a URL from an old document.
The report-only policy is worth deploying before the window so its first reports describe the legacy estate rather than your new one.

FAQ

Are protocol-relative URLs a good fix? They work and they are no longer the right answer. A //example.com/asset.js reference inherits the page’s protocol, which solved this problem in an era when sites served both. Now that HTTPS is the only protocol worth serving, the relative form just hides which scheme is intended and behaves surprisingly in contexts without a page protocol — email templates, feed readers, native apps. Write the explicit https:// and be done.

Should I use upgrade-insecure-requests instead of fixing the content? As a safety net, yes; as the fix, no. The directive tells browsers to silently upgrade insecure subresource requests, which stops the breakage immediately and is genuinely useful during a window. What it does not do is correct the stored content, so the underlying URLs remain wrong in every export, feed, and API response, and the problem reappears anywhere the header is absent. Deploy it to buy time, then fix the source.

Why did the console show nothing while the crawl found thousands? Because you were looking at pages that happen to be clean. Mixed content clusters in older content — articles written before the site moved to HTTPS, themes retired years ago, embeds from vendors who have since changed their URLs — and none of that is on the home page or in the recent articles anybody checks by hand. This is precisely the case a crawl exists for.

What about third-party embeds that only offer HTTP? Treat it as a vendor problem with a deadline rather than something to work around. Almost every reputable provider serves HTTPS now, so an HTTP-only embed usually means an outdated integration snippet rather than a genuine limitation — check their current documentation first. If a provider truly cannot serve HTTPS, the options are to proxy the resource through your own origin or to drop the embed, because there is no configuration on your side that makes an insecure script safe to load.

Does mixed content affect search rankings? Indirectly and mildly, but the user-facing effects matter more. Blocked active content can break rendering badly enough that a crawler sees a substantially different page than you intended, which is a content problem rather than a security signal. The padlock loss on passive mixed content is visible to visitors and erodes trust on exactly the pages carrying your oldest and most-linked content. Fix it for the rendering, and treat any ranking effect as a bonus.

Related

← Back to TLS & Certificate Cutover