How this check works
Scanaris requests two files at the root of your domain: /package.json, the manifest of a Node project, and /composer.json, the manifest of a PHP project. For each, it only confirms the finding if it responds 200, the body is not HTML, it is valid JSON and it contains the keys specific to that manifest: dependencies or devDependencies in the first, require or require-dev in the second. That way it does not flag a single-page app that returns its HTML for any path, nor some random JSON that happens to live there. Each file found opens a low-severity finding.
Why it matters
These files contain no secrets, but they contain something almost as useful to an attacker: the exact list of libraries you use and their versions. With that, there is nothing to guess: they cross your list with the database of known vulnerabilities and know which exploit to try and against what. They also reveal the project's internal name, the build and deploy scripts, sometimes the private repository URL and the names of your organisation's internal packages, which are the starting point of dependency confusion attacks. It is low severity because it is information, not access; but it is information you give away for no reason.
Why it happens
Because the web server serves the whole project folder instead of the output folder. A properly deployed Vite, Nuxt or Next.js only publishes dist/, .output/public/ or .next/static/, and there is no package.json there. It shows up when the full repository is uploaded over FTP to public_html, when a static host's root directory points to the project root, or in PHP projects where composer.json sits in the same directory as index.php, which is common in WordPress with Composer and in hand-made installs.
How to fix it
- Serve only the public folder. On Vercel and Netlify, check the project's "output directory" or "publish directory"; it should be the build folder, not
.. In nginx and Apache,rootorDocumentRootpoints topublic/ordist/. - In PHP, move
composer.json,composer.lockandvendor/one level above the public directory if your framework allows it (Laravel and Symfony already do). If you cannot move them, block them:
location ~* /(package|composer)(-lock)?\.json$ { return 404; }
location ~* /(yarn|pnpm-lock)\.(lock|yaml)$ { return 404; }
- In Apache, a
<FilesMatch "^(package|composer)(-lock)?\.json$"> Require all denied </FilesMatch>in the configuration or in.htaccess. - Check what else was uploaded. Where there is a public
package.jsonthere is usually a.env, a.git/or acomposer.locknext to it; the sensitive files check looks for the serious ones.
Check it is fixed
for p in package.json composer.json package-lock.json composer.lock yarn.lock; do
printf "%-20s " "$p"; curl -s -o /dev/null -w "%{http_code} %{content_type}\n" "https://yourdomain.com/$p"
done
Expected: 404 or 403. A 200 with text/html is a single-page app's generic HTML and is not a problem; a 200 with application/json is the real file.
Frequently asked questions
My repository is public on GitHub anyway.
Then the manifest is already public and this finding adds no new exposure; you can mark it as accepted. Even so, fixing the server root prevents the next file that lands in the project folder from being published by accident.
What about lockfiles?
This check looks at the two manifests, but package-lock.json, yarn.lock and composer.lock are worse: they list every transitive dependency with its exact version. If the manifest is exposed, the lockfile almost certainly is too; the blocking rule above covers them.
If I do not serve the manifest, can nobody tell which versions I use?
They can infer it by other, slower means: headers, bundle paths, behaviour. What outdated libraries detects in your JavaScript, for instance, anyone can see. Hiding the manifest removes the shortcut; updating the dependencies removes the problem.