How to check if mixed content exists on a page
Fetch the served HTML and grep it for plain HTTP subresources with curl -s https://example.com/ | grep -o -E 'src="http://[^"]*"'. Any line printed is an asset requested over HTTP from an HTTPS page. curl does not run scripts, so an empty result is a pre-filter rather than a pass, and the browser console decides.
Why check this
Mixed content is how a page that passed the HTTPS check still leaks. An image loaded over HTTP is a plain request carrying the referrer, and a script loaded over HTTP can rewrite the page. Run the grep in regression on every template that was touched, and run the browser check before release. The failure it prevents is a payment page whose analytics script is pulled over HTTP, blocked by the browser, and the form silently stops submitting.
Prerequisites
- curl 8.0 or later and grep. See the curl manual.
- Node 22 if you want a page with known mixed content to point the grep at. Save this as
mixed.mjsand runnode mixed.mjs:
import http from 'node:http';
const page = `<!doctype html><html><head>
<link rel="stylesheet" href="http://cdn.example.com/site.css">
<script src="https://cdn.example.com/app.js"></script>
</head><body>
<svg xmlns="http://www.w3.org/2000/svg"><circle r="4"/></svg>
<img src="http://images.example.com/logo.png" alt="logo">
<a href="http://blog.example.com/post">Blog post</a>
</body></html>`;
http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
res.end(page);
}).listen(8791, '127.0.0.1');
- Chrome or Firefox. MDN on mixed content explains which subresource types are blocked outright and which are upgraded.
Steps
- Step 1.
Grep the served HTML for every
http://string and count what comes back.curl -s http://127.0.0.1:8791/ | grep -o 'http://[^"]*' | sort | uniq -c1 http://blog.example.com/post 1 http://cdn.example.com/site.css 1 http://images.example.com/logo.png 1 http://www.w3.org/2000/svgFour hits, two of which are not mixed content. Read the next two steps before filing any of them.
- Step 2.
Scope the pattern to attributes, which drops the XML namespace.
curl -s http://127.0.0.1:8791/ | grep -o -E '(src|href)="http://[^"]*"' | sort -uhref="http://blog.example.com/post" href="http://cdn.example.com/site.css" src="http://images.example.com/logo.png"http://www.w3.org/2000/svgis gone. It is an SVG namespace identifier, never requested, and it appears in almost every modern page. - Step 3.
Narrow to the attributes that actually cause a request.
curl -s http://127.0.0.1:8791/ | grep -o -E 'src="http://[^"]*"|rel="stylesheet" href="http://[^"]*"' | sort -urel="stylesheet" href="http://cdn.example.com/site.css" src="http://images.example.com/logo.png"Two real findings. The
<a href>from step 2 is a navigation target, not a subresource, so it is not mixed content. - Step 4.
Run the same scoped pattern against the page you are testing and count the hits.
curl -s https://www.cloudflare.com/ | grep -c -E '(src|href)="http://[^"]*"'0The naive pattern from step 1 returns 31 hits on this same page, all of them the SVG namespace. The count that matters is this one.
- Step 5.
Open the page in Chrome, press F12, and read the Issues tab in DevTools, then the Network panel with the Protocol column shown.
The grep only sees HTML that arrived in the first response. Anything a script writes into the DOM, a CSS
url()in a stylesheet, afetchto an HTTP endpoint or a redirect that lands on HTTP is invisible to it. The browser is the only place where all of those appear, because it is the only thing that runs the page. Sort the Network panel by the Name column and look for entries whose scheme ishttp.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| src="http://…" on a script | Active mixed content | Release blocker. Browsers block it, so the feature is already broken. |
| href="http://…" on a stylesheet link | Active mixed content | Same. A blocked stylesheet changes the layout, not only the padlock. |
| src="http://…" on an image | Passive mixed content | Fix it. Browsers upgrade or block it depending on version, so behaviour varies by client. |
| http://www.w3.org/2000/svg | An XML namespace identifier | Ignore. No request is made for it. |
| href="http://…" on an anchor | A link to another page | Not mixed content. Worth fixing separately if it is your own domain. |
| Grep is clean, the browser Issues tab is not | A script or a stylesheet built the URL at runtime | Trust the browser. Find the code that composes the URL. |
Common mistakes
What to check next
- How to check HTTP to https redirect: a subresource whose host redirects to HTTPS still makes the first request in clear text.
- How to check if HSTS is enabled: HSTS on the asset host removes a class of these findings at the browser.
- How to check if cookies are secure and HttpOnly: a cookie without
Secureis attached to every mixed content request that leaves. - How to check HTTP response headers with curl: how to read what the server sent before the browser touched it.
- Security headers checker: reads the policy headers that control how the browser handles these requests.
FAQ
How to check a website for mixed content over HTTPS?
Run steps 1 to 4 on the served HTML for the pre-filter, then step 5 for the answer. The grep finds what the template hard-coded; the browser finds what the running page requested.
How to find mixed content warnings?
Open DevTools, Issues tab. The Network panel, with the Protocol column shown, lists the requests themselves so you can confirm which scheme each one used.
Why can curl not detect mixed content on its own?
curl downloads bytes. It does not parse HTML, run JavaScript, fetch stylesheets or follow subresource references, so it cannot know which URLs the page would have requested.
Does an HTTP link in an anchor tag count?
No. Mixed content covers subresources the page loads, not pages it links to. The link is worth fixing, and it belongs on a different ticket.
Is a protocol-relative URL safe?
It behaves correctly on an HTTPS page and incorrectly everywhere else. Write https:// explicitly so the markup does not depend on where it is opened.
Verified
Verified by Maks Vernycurl 8.21.0node 22.23.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate7 minpublished updated Maks Verny