Description
Mapping the entire node_modules root with Express's express.static() can make internal files directly retrievable, including package.json, source maps (.map), README files, and test data. Metadata such as package.json's _where field may reveal local absolute paths or usernames. Attackers can guess paths such as /vendor/<pkg>/package.json or /vendor/<pkg>/dist/<file>.map, then use exposed dependency versions to identify possible vulnerabilities.
Potential impact
- Exposure of internal paths, usernames, and project structure through package metadata
- Disclosure of dependency versions and configuration useful for further attacks
- Source maps or unintended files exposing code, application logic, or clues about secrets
- Easier discovery of other exposed paths or configuration mistakes
Remediation
- Do not expose the entire
node_modulesroot withexpress.static(). - Map only required public package subdirectories, such as
dist,browser, orumd, through an allow-list. - Use a bundler such as Webpack, Vite, or Rollup, or a build script, to copy required assets into a dedicated
publicdirectory and serve only that directory. - Exclude unnecessary source maps and test or documentation files from deployment.
- Review static routes, avoid unnecessary directory listings through tools such as
serve-index, and restrict static roots to the project's dedicated public directory.
Examples
Before
javascript
const express = require('express');
const path = require('path');
const app = express();
// Before: map all node_modules, exposing internal package files.
app.use('/vendor', express.static(path.join(__dirname, 'node_modules')));
app.listen(3000, () => console.log('listening on 3000'));
After
javascript
const express = require('express');
const path = require('path');
const app = express();
// After: expose only required public package subdirectories.
app.use('/vendor/jquery', express.static(path.join(__dirname, 'node_modules', 'jquery', 'dist')));
app.use('/assets', express.static(path.join(__dirname, 'public'))); // Serve only built or copied public assets.
app.listen(3000, () => console.log('listening on 3000'));
Explanation:
- Before: Exposing the root makes package metadata, source maps, and example or test files directly accessible. Internal paths, versions, and source structure can help attackers target the application.
- After: Serve only files intended to be public. Copying required assets into
publicduring the build also avoids exposingnode_modulesstructure and metadata.