Introduction
Mixed content occurs when an HTTPS page includes resources loaded over HTTP - scripts, images, stylesheets, or iframes. Browsers block or warn about this because the insecure HTTP resources compromise the security of the entire page. An attacker could intercept or modify the HTTP content, potentially injecting malicious scripts or stealing data. Fixing mixed content requires updating all resource URLs to HTTPS.
Symptoms
- Browser console warning:
Mixed Content: The page at 'https://...' was loaded over HTTPS, but requested an insecure... - Some images or scripts not loading on HTTPS pages
- Browser shows shield icon or warning in address bar
- SSL works but page feels "broken" or incomplete
- Browser blocks certain content (scripts, especially)
- Content Security Policy violations in console
- HTTPS site showing HTTP warnings in security audit
Common Causes
- Hardcoded HTTP URLs in HTML, CSS, or JavaScript
- External resources only available via HTTP
- API endpoints using HTTP instead of HTTPS
- Images from HTTP-only CDN or external sites
- Legacy code with absolute HTTP URLs
- CMS generating HTTP links in content
- Third-party integrations using HTTP
- CSS files loading HTTP background images
Step-by-Step Fix
Step 1: Identify All Mixed Content
```bash # Use browser developer tools # Open Console (F12) and look for mixed content warnings
# Or use curl to check page source curl -s https://example.com | grep -E "http://|src=\"http|href=\"http"
# Check for HTTP resources curl -s https://example.com | grep -oE 'http://[^"\' )<>]+' | sort | uniq
# Use online tools # https://www.whynopadlock.com/ # https://mixed-content-checker.com/ ```
Step 2: Categorize Mixed Content Types
Passive mixed content (images, audio, video): - Browser shows warning but usually loads content - Less security risk, but should still fix
Active mixed content (scripts, stylesheets, iframes, XHR): - Browser blocks by default in modern browsers - Major security risk, breaks page functionality
```bash # Find scripts (active) curl -s https://example.com | grep -E "<script.*http://"
# Find images (passive) curl -s https://example.com | grep -E "<img.*http://"
# Find CSS links (active) curl -s https://example.com | grep -E "<link.*http://"
# Find iframes (active) curl -s https://example.com | grep -E "<iframe.*http://" ```
Step 3: Update Internal URLs
```bash # Search and replace in source files # Replace absolute HTTP URLs with HTTPS
# For HTML files sed -i 's|http://example.com|https://example.com|g' *.html
# For WordPress database (if using WordPress) # In wp-config.php, add: define('FORCE_SSL_ADMIN', true); define('FORCE_SSL_LOGIN', true);
# Then update database URLs wp search-replace 'http://example.com' 'https://example.com' --all-tables ```
Use relative URLs instead of absolute:
```html <!-- Before --> <script src="http://example.com/js/app.js"></script> <img src="http://example.com/images/logo.png">
<!-- After - relative URLs --> <script src="/js/app.js"></script> <img src="/images/logo.png">
<!-- Or protocol-relative (deprecated but works) --> <script src="//example.com/js/app.js"></script> ```
Step 4: Update External URLs
```bash # Check if external resources support HTTPS curl -I http://external-site.com/resource.js
# If they support HTTPS, update URL # Change http:// to https://
# If external site doesn't support HTTPS, options: # 1. Find alternative HTTPS resource # 2. Host the resource yourself on HTTPS # 3. Remove the dependency if not essential ```
Step 5: Update API Endpoints
```javascript // Before fetch('http://api.example.com/data');
// After fetch('https://api.example.com/data');
// Or use relative URLs fetch('/api/data'); ```
```bash # Update backend configuration # Nginx - ensure API endpoints use HTTPS server { listen 443 ssl; server_name api.example.com;
ssl_certificate /etc/ssl/certs/api.crt; ssl_certificate_key /etc/ssl/private/api.key; }
# Redirect HTTP API to HTTPS server { listen 80; server_name api.example.com; return 301 https://$host$request_uri; } ```
Step 6: Add CSP Upgrade-Insecure-Requests
```nginx # Nginx - add header to upgrade all HTTP requests add_header Content-Security-Policy "upgrade-insecure-requests" always;
# Apache Header always set Content-Security-Policy "upgrade-insecure-requests"
# This tells browsers to automatically upgrade HTTP to HTTPS # Use as temporary fix while updating URLs ```
<!-- Or in HTML meta tag -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">Step 7: Update CMS and Framework Settings
```bash # WordPress # Settings > General > Site Address and WordPress Address # Change to HTTPS
# Drupal # Admin > Configuration > System > Site Information # Update base URL to HTTPS
# Laravel - .env file APP_URL=https://example.com
# Django - settings.py SECURE_SSL_REDIRECT = True SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') ```
Step 8: Verify All Resources Load Securely
```bash # Test page with curl curl -s https://example.com | grep "http://" | wc -l # Should be 0
# Use browser developer tools # Check Console for no mixed content warnings
# Check Network tab - all requests should be HTTPS
# Use SSL Labs # https://www.ssllabs.com/ssltest/ ```
Common Pitfalls
- Fixing some URLs but missing others in deeply nested CSS
- External resources not supporting HTTPS
- CMS regenerating HTTP URLs from database content
- API endpoints on different domains still using HTTP
- Developer tools cached old page version
- Mobile app or API clients hardcoded to HTTP
Best Practices
- Use relative URLs for internal resources
- Check external resources support HTTPS before using
- Implement CSP upgrade-insecure-requests as safety net
- Run mixed content scanner before going live with HTTPS
- Update CMS database URLs when switching to HTTPS
- Audit third-party integrations for HTTPS support
- Include mixed content check in CI/CD pipeline
Related Issues
- SSL Certificate Chain Incomplete
- HTTP to HTTPS Redirect Not Working
- SSL Certificate Name Mismatch
- HSTS Header Configuration Issues