Hoe deze controle werkt
Scanaris vraagt twee bestanden op in de root van je domein: /package.json, het manifest van een Node-project, en /composer.json, dat van een PHP-project. Voor elk bevestigt hij de bevinding alleen als het 200 antwoordt, de body geen HTML is, het geldige JSON is en het de sleutels bevat die bij dat manifest horen: dependencies of devDependencies bij het eerste, require of require-dev bij het tweede. Zo meldt hij geen single-page app die voor elk pad zijn HTML teruggeeft, en geen willekeurige JSON die daar toevallig staat. Elk gevonden bestand opent een bevinding met lage ernst.
Waarom dit belangrijk is
Deze bestanden bevatten geen geheimen, maar wel iets dat voor een aanvaller bijna even nuttig is: de exacte lijst van bibliotheken die je gebruikt en hun versies. Daarmee hoeft hij niets te raden: hij legt je lijst naast de database van bekende kwetsbaarheden en weet welke exploit hij tegen wat moet proberen. Ze onthullen ook de interne projectnaam, de build- en deployscripts, soms de URL van de privérepository en de namen van interne pakketten van je organisatie, die het startpunt zijn van dependency-confusion-aanvallen. Het is lage ernst omdat het informatie is, geen toegang; maar het is informatie die je zonder reden weggeeft.
Waarom het gebeurt
Omdat de webserver de hele projectmap serveert in plaats van de uitvoermap. Een goed gedeployde Vite, Nuxt of Next.js publiceert alleen dist/, .output/public/ of .next/static/, en daar staat geen package.json. Het duikt op wanneer de volledige repository via FTP naar public_html wordt geüpload, wanneer de rootmap van een statische host naar de projectroot wijst, of in PHP-projecten waar composer.json in dezelfde map staat als index.php, wat gebruikelijk is bij WordPress met Composer en bij handmatige installaties.
Hoe je het oplost
- Serveer alleen de openbare map. Controleer op Vercel en Netlify de "output directory" of "publish directory" van het project; dat moet de buildmap zijn, niet
.. In nginx en Apache wijstrootofDocumentRootnaarpublic/ofdist/. - In PHP verplaats je
composer.json,composer.lockenvendor/één niveau boven de openbare map als je framework dat toelaat (Laravel en Symfony doen dat al). Kun je ze niet verplaatsen, blokkeer ze dan:
location ~* /(package|composer)(-lock)?\.json$ { return 404; }
location ~* /(yarn|pnpm-lock)\.(lock|yaml)$ { return 404; }
- In Apache een
<FilesMatch "^(package|composer)(-lock)?\.json$"> Require all denied </FilesMatch>in de configuratie of in de.htaccess. - Controleer wat er nog meer is geüpload. Waar een openbare
package.jsonstaat, staat meestal een.env, een.git/of eencomposer.lockernaast; de controle gevoelige bestanden zoekt de ernstige gevallen.
Controleer of het opgelost is
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://jouwdomein.nl/$p"
done
Verwacht: 404 of 403. Een 200 met text/html is de generieke HTML van een single-page app en is geen probleem; een 200 met application/json is het echte bestand.
Veelgestelde vragen
Mijn repository is toch al openbaar op GitHub.
Dan is het manifest al openbaar en voegt deze bevinding geen nieuwe blootstelling toe; je kunt hem als geaccepteerd markeren. Toch voorkomt het herstellen van de serverroot dat het volgende bestand dat in de projectmap belandt per ongeluk wordt gepubliceerd.
En lockfiles?
Deze controle kijkt naar de twee manifesten, maar package-lock.json, yarn.lock en composer.lock zijn erger: ze noemen elke transitieve afhankelijkheid met exacte versie. Is het manifest blootgesteld, dan is het lockfile dat vrijwel zeker ook; de blokkeerregel hierboven dekt ze.
Als ik het manifest niet serveer, kan dan niemand weten welke versies ik gebruik?
Ze kunnen het via andere, tragere wegen afleiden: headers, bundelpaden, gedrag. Wat verouderde bibliotheken in je JavaScript detecteert, ziet bijvoorbeeld iedereen. Het manifest verbergen haalt de sluiproute weg; de afhankelijkheden bijwerken haalt het probleem weg.