Skip to main content
interlace
Plugin: express-securityRules

no-static-root-exposure

Disallow express.static() roots that expose the application directory and any serve-index usage

Keywords: static files, express.static, serve-index, directory listing, CWE-548, information exposure, __dirname, path traversal, .env exposure, ESLint rule, LLM-optimized

Detects static-file middleware that serves the application root (__dirname, '.', process.cwd(), path.join(__dirname) with no subdirectory or with .. segments) and any use of serve-index directory listings. This rule is part of eslint-plugin-express-security and provides LLM-optimized error messages.

⚠️ This rule errors by default in the recommended config.

Quick Summary

AspectDetails
CWE ReferenceCWE-548 (Exposure of Information Through Directory Listing)
Severity🔴 High
Auto-Fix💡 Suggestion (scopes the root to path.join(__dirname, 'public'))
CategorySecurity
Best ForExpress.js apps serving static assets

Value & investment case

Why this rule pays for itself. Framework: cicd-impact/philosophy.md.

DimensionValue
CWECWE-548 — Exposure of Information Through Directory Listing
Feedback-loop tierEditor / pre-commit (sub-second) — cheapest layer per the feedback-loop hierarchy
Defensive-layer leverage~10× cheaper than unit-test · ~1,000× cheaper than production rollback · 10,000+× cheaper than customer disclosure (cost-ratio anchors)
Niche relevanceCritical: any app with secrets in .env / config files (i.e. nearly all) · High: B2B SaaS, fintech (compliance exposure) · Medium: static marketing sites
Investor-frame impactexpress.static(__dirname) publishes .env, package-lock.json, .git metadata and the server source itself — a one-line misconfiguration that equals a full credential + source-code leak. Directory listings hand attackers a filesystem map. Catch at lint-time prevents the incident entirely.

Read also: philosophy.md §investor-frame · niche-presets.json · analyzer-evaluation-framework.md

Vulnerability and Risk

Vulnerability: Pointing express.static() at the application root (or any path that resolves outside a dedicated asset directory) makes every file in the project publicly downloadable. Adding serve-index renders a browsable listing of them.

Risk: Attackers download .env (API keys, DB credentials), lockfiles (dependency fingerprinting for CVE targeting), .git internals (full source history), and the server source itself. This is a complete-compromise primitive that requires no exploit — just a URL.

How the Attack Works

Rule Logic Flow

Detection Patterns

PatternRiskDescription
express.static(__dirname)🔴 CriticalServes the entire application directory
express.static('.') / ('..') / ('/')🔴 CriticalServes the app root or above
express.static(process.cwd())🔴 CriticalServes the working directory
express.static(path.join(__dirname))🔴 CriticalJoin with no subdirectory
path.join(__dirname, '..', 'shared')🔴 High.. escapes the application directory
path.join(__dirname, dir)🟡 MediumNon-literal segment — unverifiable
path.join(__dirname, 'uploads')🟡 MediumDirectory not in the allowlist
serveIndex(...) (any arguments)🔴 HighDirectory listing enabled

Examples

❌ Incorrect

const express = require('express');
const serveIndex = require('serve-index');
const path = require('path');

// Application root - VULNERABLE (.env, .git, source all public)
app.use(express.static(__dirname));

// Relative root / cwd - VULNERABLE
app.use(express.static('.'));
app.use(express.static(process.cwd()));

// Join without a subdirectory - VULNERABLE
app.use(express.static(path.join(__dirname)));

// Escaping the app directory - VULNERABLE
app.use(express.static(path.join(__dirname, '..', 'shared')));

// Directory listing - VULNERABLE
app.use(serveIndex('/', { icons: true }));

✅ Correct

const express = require('express');
const path = require('path');

// Dedicated asset directory - SAFE
app.use(
  express.static(path.join(__dirname, 'public'), {
    index: 'index.html',
    dotfiles: 'ignore',
  }),
);

// Any allowlisted root - SAFE
app.use(express.static('public'));
app.use(express.static(path.join(__dirname, 'dist')));
app.use(express.static(path.join(process.cwd(), 'assets')));

// Custom directory via the allowedRoots option - SAFE
// { allowedRoots: ['www'] }
app.use(express.static(path.join(__dirname, 'www')));

Error Message Format

The rule provides LLM-optimized error messages (Compact 2-line format) with actionable security guidance:

🔒 CWE-548 | Application Root Served Statically (CWE-548) | HIGH
   Fix: Serve a dedicated asset directory instead: express.static(path.join(__dirname, 'public')) | https://cwe.mitre.org/data/definitions/548.html

Message Components

ComponentPurposeExample
Risk StandardsSecurity benchmarksCWE-548
Issue DescriptionSpecific vulnerabilityApplication Root Served Statically / Directory Listing Enabled
Severity & ComplianceImpact assessmentHIGH / MEDIUM
Fix InstructionActionable remediationServe a dedicated asset directory
Technical TruthOfficial referenceCWE-548

Configuration

{
  rules: {
    "express-security/no-static-root-exposure": ["error", {
      allowedRoots: ["public", "static", "dist", "build", "assets"]
    }]
  }
}

Options

OptionTypeDefaultDescription
allowedRootsstring[]['public', 'static', 'dist', 'build', 'assets']Directory names accepted as the first path segment of a static root (replaces the default set; the first entry seeds the fix suggestion)

Best Practices

1. One Dedicated Asset Directory

app.use(
  express.static(path.join(__dirname, 'public'), {
    dotfiles: 'ignore',
    index: 'index.html',
  }),
);

2. Never Enable Directory Listings in Production

// Serve an explicit index or a curated file list instead of serve-index
app.get('/files', (req, res) => res.json(CURATED_FILE_LIST));

3. Extend the Allowlist Instead of Disabling the Rule

// eslint config — the team serves from www/
"express-security/no-static-root-exposure": ["error", { allowedRoots: ["www"] }]

Known False Negatives

The following patterns are not detected due to static analysis limitations:

Root Held in a Variable

Why: The rule does not resolve variables to their values (no taint analysis).

// ❌ NOT DETECTED - Root behind a variable
const root = __dirname;
app.use(express.static(root));

Mitigation: Inline the root expression at the express.static() call site.

Renamed or Destructured static

Why: Only the express.static member shape is matched.

// ❌ NOT DETECTED - Destructured binding
const { static: serveStatic } = require('express');
app.use(serveStatic(__dirname));

Mitigation: Use the canonical express.static(...) form.

serve-static Package

Why: The standalone serve-static package is a different module surface.

// ❌ NOT DETECTED - serve-static used directly
const serveStatic = require('serve-static');
app.use(serveStatic(__dirname));

Mitigation: Prefer express.static (it wraps serve-static) so the rule can see the root.

Computed Member Access

Why: express['static'] and path['join'] are computed accesses that are not matched.

// ❌ NOT DETECTED - Computed access
app.use(express['static']('.'));

Mitigation: Avoid computed access for middleware factories.

Resources

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.