Static Files¶
Static file serving sounds simple. Serve files from a directory. But misconfigure it and you can serve your entire source code including .env files , node_modules , and database credentials
Express's express.static() is a built-in middleware that serves files from a directory. Simple on the surface. Dangerous underneath
express.static() basics¶
const express = require('express')
const app = express()
// serve everything in 'public' directory
app.use(express.static('public'))
Now any file in public/ is accessible via URL:
flowchart LR
root["public/"] --> css["css/"]
css --> sc["style.css"]
sc --> rcss["/css/style.css"]
root --> js["js/"]
js --> aj["app.js"]
aj --> rjs["/js/app.js"]
root --> img["images/"]
img --> lp["logo.png"]
lp --> ri["/images/logo.png"]
root --> ih["index.html"]
ih --> rih["/index.html"] multiple static directories¶
app.use(express.static('public'))
app.use(express.static('uploads'))
app.use(express.static('dist'))
Express checks each directory in order and serves the first match. Same filename in multiple directories? First directory wins
virtual path prefix¶
Serve files under a URL prefix that doesn't match the filesystem path
// files in 'public' appear under '/static'
app.use('/static' , express.static('public'))
// public/css/style.css -> /static/css/style.css
// public/js/app.js -> /static/js/app.js
This is a security move too - attackers don't learn your filesystem structure
caching headers¶
Express sets Cache-Control and ETag headers by default
// override default caching
app.use('/static' , express.static('public' , {
maxAge: '1d', // cache for 1 day
etag: true, // enable ETag (default)
lastModified: true, // enable Last-Modified (default)
setHeaders: (res , path) => {
if (path.endsWith('.html')) {
// don't cache HTML files
res.setHeader('Cache-Control' , 'no-cache')
}
}
}))
Production tip: Set far-future Cache-Control for hashed filenames (style.a1b2c3.css) and short/no cache for non-hashed files
security - hide directory listing¶
By default Express does NOT serve directory listings. Good. Keep it that way
// This is SAFE - Express doesn't list directories
app.use(express.static('public'))
If you're using a different static file tool (like serve-index), never enable it in production Directory listing gives attackers a map of your application assets , versioned filenames , and potential backup files
what NOT to serve¶
Never ever serve these directories:
// DANGEROUS - serves everything including source
app.use(express.static('.')) // serves entire project
app.use(express.static('../')) // serves parent directory
// SLIGHTLY LESS DANGEROUS but still bad
app.use(express.static('src')) // serves source code
app.use(express.static('node_modules')) // serves all dependencies
What happens if you serve the project root:
GET /package.json -> your dependencies , scripts , project name
GET /.env -> database credentials , API keys
GET /server.js -> your application source code
GET /node_modules/ -> all 50000 dependencies accessible
dotfiles option¶
Control how files starting with . are handled:
// default: 'ignore' - dotfiles are not served
app.use(express.static('public'))
// explicit options:
app.use(express.static('public' , {
dotfiles: 'ignore' // don't serve .env , .gitignore (DEFAULT)
// dotfiles: 'allow' // serve dotfiles (DANGEROUS)
// dotfiles: 'deny' // return 403 for dotfiles
}))
Keep the default. There is never a reason to serve dotfiles
path traversal prevention¶
Express.static prevents path traversal attacks by default - GET /../../../etc/passwd won't work
// This is SAFE - Express normalizes the path
// GET /../../../etc/passwd -> 404
// GET /..%2f..%2f..%2fetc/passwd -> 404 (if properly configured)
But don't rely on this alone. Add your own checks:
app.use('/static' , (req , res , next) => {
// block path traversal attempts explicitly
if (req.path.includes('..') || req.path.includes('%2e')) {
return res.status(403).send('Forbidden')
}
next()
}, express.static('public'))
complete production setup¶
const express = require('express')
const path = require('path')
const app = express()
// static files under /static prefix
app.use('/static' , express.static(path.join(__dirname , 'public') , {
maxAge: process.env.NODE_ENV === 'production' ? '7d' : 0,
etag: true,
lastModified: true,
dotfiles: 'deny'
}))
// directly serve index.html at root
app.get('/' , (req , res) => {
res.sendFile(path.join(__dirname , 'public' , 'index.html'))
})
prerequisites¶
express_04_middleware.md - middleware patterns , app.use() , execution order
next → express_06_error_handling.md