How this check works
Scanaris parses the HTML of your main page and walks through every element with a src, href or action attribute: scripts, images, stylesheets, links, forms. For each value it checks whether it points to a host that should not exist outside your machine or your network: localhost, 127.0.0.1, 0.0.0.0, the private IP ranges 10.x.x.x, 192.168.x.x and 172.16.x.x through 172.31.x.x, and any domain ending in .local. If it finds any, it opens a low-severity finding with the first references. Only the initial HTML is examined; whatever JavaScript builds afterwards is not seen this way.
Why it matters
A reference to http://localhost:3000/api/... in production has two consequences. The first is functional: to your user, localhost is their own computer, so that image does not load, that form submits to nowhere and that script fails silently. The second is informational: you are telling anyone that your API runs on port 3000, that your internal backend is called api.company.local or that your network uses the 10.0.1.x range. None of that is an exploitable flaw on its own, hence the low severity, but they are pieces an attacker keeps for later, and they also show the deployment went out without review.
Why it happens
It is almost always a URL typed by hand during development that nobody changed. The AI that generated the project put http://localhost:8000 in the form's fetch, or in the src of a test image, and it worked locally, so it stayed. Other causes: a development .env that ended up in the production build, a WordPress migrated from a local environment with the site URL never updated, or a form action copied from an example.
How to fix it
- Take URLs out of the code. Your API base goes in an environment variable (
NUXT_PUBLIC_API_BASE,NEXT_PUBLIC_API_URL,VITE_API_URL) with a different value per environment. In production, the platform (Vercel, Netlify, Railway) injects it at build time. - Use relative paths whenever the resource lives on your own domain:
/api/contactinstead ofhttp://localhost:3000/api/contact. They work the same locally and in production. - WordPress: update the site URLs under General Settings and replace the old URLs in the database with a tool that respects serialisation.
- Search the build before deploying. A CI step that fails if the compiled output contains these hosts cuts the problem at the root:
grep -rEl "localhost|127\.0\.0\.1|192\.168\.|\.local\b" dist/ .output/public/ 2>/dev/null
If any file comes out, there is a reference to fix before shipping.
Check it is fixed
curl -s https://yourdomain.com | grep -oE '(src|href|action)="https?://[^"]+"' | grep -iE "localhost|127\.0\.0\.1|0\.0\.0\.0|10\.[0-9]+\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|\.local\b"
Nothing should come out. Also try one or two inner pages, especially those with forms.
Frequently asked questions
It is a test image nobody sees. Does it matter?
Every visitor's browser tries to load it from their own computer, and the HTML keeps announcing your internal host. Change it or delete it; it takes a minute.
Is the compiled JavaScript checked too?
Not by this check, only the HTML. But an internal URL in the bundle usually comes with other things that should not be there; secrets in JavaScript looks for keys and tokens, and HTML comments looks for leftovers in the HTML itself. The three together cover what usually remains of a development environment.
My link points to a .local domain on purpose.
.local domains are reserved for local networks (mDNS) and do not resolve on the internet. If you want to link an internal tool, use a real subdomain with authentication. What your app leaks without meaning to, with more examples, is in secrets your app leaks in the browser.