Information exposure through static node_modules hosting

Information exposure

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_modules root with express.static().
  • Map only required public package subdirectories, such as dist, browser, or umd, through an allow-list.
  • Use a bundler such as Webpack, Vite, or Rollup, or a build script, to copy required assets into a dedicated public directory 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 public during the build also avoids exposing node_modules structure and metadata.

References