Loading content...
You've deployed your site over HTTPS, obtained valid certificates, and watched the padlock appear in browsers. Your site is secure, right? Not necessarily.
A critical vulnerability lurks in many HTTPS deployments: mixed content. When an HTTPS page loads resources (scripts, images, stylesheets, iframes) over unencrypted HTTP connections, the security guarantees of HTTPS are fundamentally compromised.
Mixed content is particularly insidious because it's invisible to casual users, may only appear under specific conditions, and can completely undermine the authentication and encryption HTTPS provides. Understanding and preventing mixed content is essential for any engineer deploying secure web applications.
In this comprehensive exploration, we'll dissect the mixed content problem—what it is, why it's dangerous, how browsers protect against it, and how to detect and eliminate it from your applications.
By the end of this page, you will understand: (1) What mixed content is and why it's dangerous, (2) The difference between active and passive mixed content, (3) How browsers handle mixed content, (4) Detection methods and tools, (5) Remediation strategies, and (6) Content Security Policy for mixed content protection.
Mixed content occurs when an initial HTML document is loaded securely over HTTPS, but that document then loads sub-resources (scripts, stylesheets, images, videos, iframes, etc.) over insecure HTTP connections.
The Fundamental Problem:
HTTPS provides three guarantees:
Mixed content breaks all three guarantees for the HTTP resources:
https://secure-bank.com/dashboard
│
├── loads https://secure-bank.com/app.js ✓ Protected
├── loads https://secure-bank.com/style.css ✓ Protected
│
└── loads http://cdn.example.com/widget.js ✗ EXPOSED!
↑
Man-in-the-middle can:
- Read all data in this request
- Modify the response (inject malicious code)
- Impersonate the resource server
An attacker on the same network (coffee shop WiFi, compromised router, ISP-level) can intercept HTTP requests. If your banking page loads one HTTP JavaScript file, the attacker can inject code that steals credentials, transfers funds, or installs malware—all while the user sees the padlock and thinks they're secure.
Why Does Mixed Content Exist?
Mixed content typically appears due to:
http:// explicitly written in HTML/CSS/JS//example.com/resource on HTTP sites later migrated to HTTPSRequest Flow Comparison:
Not all mixed content is equally dangerous. Browsers distinguish between two categories based on the potential impact of compromise:
Active Mixed Content (High Risk)
Active mixed content can modify the DOM, execute code, or interact with the page. Compromise of active content gives attackers full control of the page.
Blocked by default in all modern browsers.
| Resource Type | HTML Element | Risk |
|---|---|---|
| JavaScript | <script src="http://..."> | Execute arbitrary code, steal all data |
| CSS Stylesheets | <link rel="stylesheet" href="http://..."> | Can exfiltrate data via CSS injection |
| Iframes | <iframe src="http://..."> | Full page takeover, phishing |
| XMLHttpRequest/Fetch | fetch('http://...') | Data exfiltration, CSRF |
| Web Workers | new Worker('http://...') | Background code execution |
| Fonts (@font-face) | @font-face { src: url('http://...') } | Limited, but possible exploits |
| Object/Embed | <object data="http://..."> | Plugin code execution |
Passive (Display) Mixed Content (Lower Risk)
Passive mixed content cannot execute code or directly access page content. The impact of compromise is limited to the content itself.
May be loaded with warnings, or blocked in stricter modes.
| Resource Type | HTML Element | Risk |
|---|---|---|
| Images | <img src="http://..."> | Display falsified images, track users |
| Video | <video src="http://..."> | Replace video content, tracking |
| Audio | <audio src="http://..."> | Replace audio, limited tracking |
While passive content can't execute code, it's still dangerous: (1) Attackers can replace images with misleading content (fake 'download' buttons, phishing), (2) Unique tracking pixels expose user behavior, (3) Visible HTTP warning indicators erode user trust, (4) Some 'passive' contexts can become active through browser bugs.
Why the Distinction?
Browsers make this distinction for practical migration reasons:
The Evolution:
| Era | Active Mixed Content | Passive Mixed Content |
|---|---|---|
| Early 2010s | Warning only | No warning |
| Mid 2010s | Blocked by default | Warning shown |
| Late 2010s | Blocked | Blocked or upgraded |
| 2020s | Blocked | Auto-upgraded to HTTPS |
Modern browsers (Chrome 80+) attempt to auto-upgrade passive mixed content to HTTPS. If the HTTPS version fails, the resource is blocked.
Browsers implement sophisticated mixed content protection, but behavior varies across browsers and evolves over time.
Chrome Mixed Content Behavior (2024):
Visual Indicators:
🔒 Secure (HTTPS, no mixed content)
→ "Connection is secure" in address bar
⚠️ Not Secure (mixed content detected)
→ Warning triangle, "Not secure" or "Mixed content"
🔓 Not Secure (HTTP page or severe issues)
→ "Not secure" prominently displayed
Console Errors and Warnings:
// Active mixed content (script)
Mixed Content: The page at 'https://example.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.
// Passive mixed content (image, before auto-upgrade)
Mixed Content: The page at 'https://example.com/' was loaded over HTTPS,
but requested an insecure image 'http://cdn.example.com/photo.jpg'.
This content should also be served over HTTPS.
// Auto-upgrade notification
Automatically upgrading a mixed content request from
'http://cdn.example.com/photo.jpg' to 'https://cdn.example.com/photo.jpg'
Firefox Behavior:
Similar to Chrome with some differences:
Safari Behavior:
Chrome's auto-upgrade feature attempts to load HTTP content over HTTPS instead. This helps sites that serve content over both protocols but have HTTP hardcoded in their pages. However, if the HTTPS version doesn't exist or fails, the content is blocked. This is why ensuring all resources are available over HTTPS is essential.
Mixed Content in Different Contexts:
| Context | Active Content | Passive Content |
|---|---|---|
| Top-level HTTPS page | Blocked | Auto-upgraded |
| HTTPS iframe on HTTPS page | Blocked | Auto-upgraded |
| HTTP iframe on HTTPS page | Blocked entirely | Blocked entirely |
| HTTPS worker on HTTPS page | Blocked | N/A |
| Service Worker scope | Must be same-origin HTTPS | N/A |
The upgrade-insecure-requests Directive:
CSP can instruct browsers to upgrade all requests:
Content-Security-Policy: upgrade-insecure-requests
This tells the browser to automatically rewrite all HTTP URLs to HTTPS before making requests. Unlike browser auto-upgrade, this applies to ALL resources, not just passive content.
Finding all mixed content in a web application requires systematic detection across multiple sources.
Method 1: Browser Developer Tools
The most immediate approach—check the console after loading pages over HTTPS:
1. Open DevTools (F12)
2. Navigate to Console tab
3. Filter by "Mixed Content" or look for warnings
4. Check Network tab for HTTP requests
- Filter: "scheme:http" or look for 🔓 icons
Limitations: Only detects mixed content on pages you visit, doesn't catch dynamic content or edge cases.
Method 2: Content Security Policy Reporting
Deploy CSP in report-only mode to collect violations:
Content-Security-Policy-Report-Only:
default-src https:;
report-uri https://example.com/csp-reports;
report-to csp-endpoint;
Report-To: {
"group": "csp-endpoint",
"max_age": 86400,
"endpoints": [
{ "url": "https://example.com/csp-reports" }
]
}
Browsers will report violations without blocking, allowing you to discover mixed content from real user traffic.
CSP Report Format:
{
"csp-report": {
"document-uri": "https://example.com/page",
"violated-directive": "img-src https:",
"blocked-uri": "http://cdn.example.com/image.jpg",
"line-number": 42,
"source-file": "https://example.com/app.js"
}
}
Method 3: Automated Scanners
Tools that crawl your site and identify mixed content:
# WebPageTest (includes mixed content detection)
https://www.webpagetest.org
# SSL Labs (certificate check + mixed content)
https://www.ssllabs.com/ssltest/
# Why No Padlock (targeted mixed content finder)
https://www.whynopadlock.com/
# Lighthouse (Chrome DevTools or CLI)
lighthouse https://example.com --output json
Method 4: Source Code Analysis
Grep for HTTP URLs in your codebase:
# Find hardcoded HTTP URLs in code
grep -rn 'http://' --include='*.html' --include='*.js' --include='*.css' .
# More targeted search
grep -rn 'src="http://' --include='*.html' .
grep -rn "src='http://" --include='*.html' .
grep -rn 'url(http://' --include='*.css' .
src=, href=, action= with HTTP URLsurl(http://...) in stylesheetsCSP reporting should be always-on in production. New features, third-party updates, or content changes can introduce mixed content at any time. Automated monitoring catches issues before users report them.
Once mixed content is identified, remediation follows a systematic approach. The strategy depends on the source of the mixed content.
Strategy 1: Use Protocol-Relative URLs (Transitional)
<!-- Instead of -->
<script src="http://cdn.example.com/lib.js"></script>
<!-- Use -->
<script src="//cdn.example.com/lib.js"></script>
Protocol-relative URLs inherit the page's protocol. On HTTPS pages, they become HTTPS requests.
⚠️ Warning: This is a transitional solution. If the HTTPS version doesn't exist, this will break. Modern best practice is explicit HTTPS.
Strategy 2: Use Explicit HTTPS URLs (Recommended)
<!-- Best practice: explicit HTTPS -->
<script src="https://cdn.example.com/lib.js"></script>
<img src="https://images.example.com/photo.jpg">
<link rel="stylesheet" href="https://example.com/style.css">
This is the most reliable approach—ensures HTTPS is always used, fails loudly if HTTPS isn't available.
Strategy 3: Update Server URL Generation
Backend code often constructs URLs. Ensure HTTPS is used:
# Python/Django
from django.contrib.sites.models import Site
# Bad: may return HTTP
site = Site.objects.get_current()
url = f"http://{site.domain}/path"
# Good: use request scheme or force HTTPS
url = request.build_absolute_uri('/path') # Uses request's scheme
url = f"https://{site.domain}/path" # Explicit HTTPS
// Node.js/Express
// Bad
const url = `http://${req.headers.host}/path`;
// Good: check for HTTPS or X-Forwarded-Proto
const protocol = req.secure || req.headers['x-forwarded-proto'] === 'https'
? 'https' : 'http';
const url = `${protocol}://${req.headers.host}/path`;
// Best: always use HTTPS in production
const url = `https://${req.headers.host}/path`;
Strategy 4: Configure CDNs and Third Parties
Ensure all external services support HTTPS:
CDN Configuration:
├── Verify SSL/TLS is enabled on CDN
├── Update CDN origin to use HTTPS
├── Force HTTPS in CDN settings
└── Update all CDN URLs in application
Third-Party Services:
├── Google Analytics: Already HTTPS
├── Font services: Use HTTPS URLs
├── Ad networks: Contact for HTTPS support
└── Widgets: Update embed codes
Strategy 5: Use CSP upgrade-insecure-requests
For sites with many HTTP references, CSP can auto-upgrade:
# Upgrade all HTTP requests to HTTPS
Content-Security-Policy: upgrade-insecure-requests
How it works:
This is a transition tool, not a permanent solution—fix the underlying URLs.
Before deploying upgrade-insecure-requests, ensure all your resources are available over HTTPS. The directive will break resources that don't support HTTPS. Test thoroughly in staging with real content.
Strategy 6: Handle User-Generated Content
User content (comments, profiles, CMS entries) may contain HTTP links:
// Sanitize on input or output
function sanitizeContent(html) {
// Replace HTTP with protocol-relative for same-site resources
return html.replace(
/src=["']http:\/\/example\.com\//g,
'src="https://example.com/'
);
}
// Or convert all image sources to HTTPS
function upgradeImageUrls(html) {
return html.replace(
/<img([^>]*)src=["']http:\/\//gi,
'<img$1src="https://'
);
}
For truly external HTTP images, consider:
Content Security Policy (CSP) provides the most robust defense against mixed content by explicitly declaring what sources are permitted for each resource type.
Basic HTTPS-Enforcing CSP:
Content-Security-Policy: default-src https:; script-src https:; style-src https:; img-src https: data:; font-src https: data:;
This policy:
data: URIs for inline images and fonts (common requirement)Strict CSP with Specific Sources:
Content-Security-Policy:
default-src 'none';
script-src 'self' https://cdn.example.com;
style-src 'self' https://fonts.googleapis.com;
img-src 'self' https://images.example.com data:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-src https://www.youtube.com;
base-uri 'self';
form-action 'self';
CSP Directives for Mixed Content:
| Directive | Controls | Example |
|---|---|---|
default-src | Fallback for all resource types | default-src https: |
script-src | JavaScript sources | script-src https://cdn.example.com |
style-src | CSS sources | style-src 'self' https: |
img-src | Image sources | img-src https: data: |
font-src | Font sources | font-src https://fonts.gstatic.com |
connect-src | Fetch/XHR/WebSocket | connect-src https://api.example.com |
frame-src | iframe sources | frame-src https: |
media-src | Audio/video | media-src https: |
object-src | Plugins (Flash, etc.) | object-src 'none' |
block-all-mixed-content | Block all mixed content | block-all-mixed-content (deprecated) |
upgrade-insecure-requests | Auto-upgrade HTTP to HTTPS | upgrade-insecure-requests |
block-all-mixed-content vs upgrade-insecure-requests:
# Block approach: any HTTP request fails
Content-Security-Policy: block-all-mixed-content
# Upgrade approach: try HTTPS first, fail if unavailable
Content-Security-Policy: upgrade-insecure-requests
upgrade-insecure-requests is generally preferred—it provides a migration path while still ensuring security.
Start with CSP in report-only mode to discover violations without breaking your site. Once you've fixed issues, switch to enforcing mode. Use Content-Security-Policy-Report-Only with identical directives to continue monitoring even after enforcement.
Implementing CSP:
# Nginx
add_header Content-Security-Policy "default-src https:; upgrade-insecure-requests;" always;
# Report-only mode for testing
add_header Content-Security-Policy-Report-Only "default-src https:; report-uri /csp-reports;" always;
// Express.js using helmet
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'", "https:"],
scriptSrc: ["'self'", "https://cdn.example.com"],
imgSrc: ["'self'", "https:", "data:"],
upgradeInsecureRequests: [],
},
}));
<!-- Meta tag (limited, can't use report-uri) -->
<meta http-equiv="Content-Security-Policy"
content="default-src https:; upgrade-insecure-requests;">
Mixed content detection and prevention has several edge cases that can trip up even experienced developers.
<link rel="icon" href="https://..."> explicitly.//# sourceMappingURL= can be HTTP. Not a security issue but may confuse debuggers.ws:// on an HTTPS page is mixed content. Use wss:// for secure WebSockets.The Referrer-Policy Interaction:
When following HTTP links from HTTPS pages, the Referrer header behavior depends on Referrer-Policy:
# Strict: no referrer to HTTP destinations
Referrer-Policy: strict-origin-when-cross-origin
# With this policy:
https://example.com → https://other.com → Referrer: https://example.com/
https://example.com → http://other.com → Referrer: (none)
This is a security feature—HTTPS page URLs shouldn't leak to HTTP destinations.
The <base> Tag Trap:
<!-- If your page has -->
<base href="http://example.com/">
<!-- Then relative URLs become HTTP -->
<script src="/app.js"></script> <!-- Becomes http://example.com/app.js -->
Ensure <base> tags use HTTPS or remove them entirely.
An HTTPS page embedding an HTTP iframe is mixed content and will be blocked. But an HTTPS iframe from a third party may itself load HTTP resources—and that's harder to detect. You're trusting the iframe source to also be fully secure.
Migrating a site from HTTP to HTTPS while avoiding mixed content requires systematic planning.
Phase 1: Audit (Before Migration)
Phase 2: Prepare Resources
Phase 3: Deploy with CSP
# Start with report-only to detect issues
Content-Security-Policy-Report-Only:
default-src https:;
upgrade-insecure-requests;
report-uri /csp-reports;
Monitor reports, fix violations, iterate.
Phase 4: Migration Execution
upgrade-insecure-requests CSPPhase 5: Lock Down with HSTS
Once confident there's no mixed content:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
This instructs browsers to always use HTTPS, preventing any HTTP requests.
Phase 6: Ongoing Monitoring
Add mixed content checks to your CI/CD pipeline using tools like Lighthouse or custom scripts. Catch mixed content introduction before it reaches production.
We've comprehensively explored mixed content—the subtle vulnerability that can undermine HTTPS security. Let's consolidate the essential concepts:
What's Next:
With mixed content understood, the next page examines HTTP Strict Transport Security (HSTS)—the mechanism that instructs browsers to always use HTTPS, preventing protocol downgrade attacks and ensuring connections start secure.
You now understand mixed content thoroughly—from the security risks through browser handling, detection, and remediation. Apply this knowledge to audit your applications and ensure HTTPS provides its full security benefits.