Information Disclosure from Enabled Directory Listing via serve-index in Express

Medium Risk Information Disclosure
JavaScriptExpressInformation DisclosureDirectory ListingFile Systemserve-index

What it is

Express applications using serve-index middleware without proper restrictions expose directory contents to attackers, potentially revealing sensitive files, source code, configuration files, and other confidential information.

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

// Vulnerable: Exposing sensitive directories
app.use('/uploads', express.static('uploads'), serveIndex('uploads', {'icons': true}));
app.use('/backups', express.static('backups'), serveIndex('backups'));
app.use('/logs', express.static('logs'), serveIndex('logs'));
app.use('/config', express.static('config'), serveIndex('config'));

// Vulnerable: No authentication or filtering
app.use('/admin-files', express.static('admin'), serveIndex('admin'));

// Vulnerable: Exposing application root
app.use('/app', express.static('.'), serveIndex('.', {'icons': true}));

// This exposes:
// - Configuration files (.env, config.json)
// - Source code files (.js, .ts)
// - Database files
// - SSL certificates
// - User uploaded files
// - Log files with sensitive data
// - Backup files
// - Node modules
const express = require('express');
const path = require('path');
const fs = require('fs').promises;
const app = express();

// Secure: No directory listing in production
if (process.env.NODE_ENV === 'development') {
    // Development-only directory listing with restrictions
    const serveIndex = require('serve-index');
    
    app.use('/dev-uploads', 
        authenticateAdmin,
        express.static('dev-uploads'), 
        serveIndex('dev-uploads', {
            'icons': true,
            'filter': (name, index, files, dir) => {
                // Only show safe file types
                const safeExtensions = /\.(jpg|jpeg|png|gif|pdf|txt)$/i;
                return safeExtensions.test(name) && 
                       !name.startsWith('.') &&  // Hide hidden files
                       !name.includes('sensitive'); // Hide files with 'sensitive' in name
            },
            'template': path.join(__dirname, 'views/directory-listing.html')
        })
    );
}

// Secure: Controlled file serving for production
const ALLOWED_FILE_TYPES = ['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.txt'];
const UPLOADS_DIR = path.resolve('./secure-uploads');

// API endpoint to list files with authorization
app.get('/api/files', authenticateUser, async (req, res) => {
    try {
        const userFolder = path.join(UPLOADS_DIR, req.user.id);
        
        // Ensure user folder exists
        await fs.mkdir(userFolder, { recursive: true });
        
        const files = await fs.readdir(userFolder);
        
        const fileList = files
            .filter(file => {
                const ext = path.extname(file).toLowerCase();
                return ALLOWED_FILE_TYPES.includes(ext);
            })
            .map(file => ({
                name: file,
                downloadUrl: `/api/files/${encodeURIComponent(file)}`,
                uploadDate: getFileUploadDate(file) // Implement this function
            }));
        
        res.json({ files: fileList });
    } catch (error) {
        console.error('Error listing user files:', error);
        res.status(500).json({ error: 'Failed to list files' });
    }
});

// Secure file download with authorization
app.get('/api/files/:filename', authenticateUser, async (req, res) => {
    try {
        const filename = req.params.filename;
        const userId = req.user.id;
        
        // Validate filename
        if (!isSecureFilename(filename)) {
            return res.status(400).json({ error: 'Invalid filename' });
        }
        
        // Construct secure file path
        const filePath = path.join(UPLOADS_DIR, userId, filename);
        const resolvedPath = path.resolve(filePath);
        const userDir = path.resolve(UPLOADS_DIR, userId);
        
        // Ensure file is within user's directory
        if (!resolvedPath.startsWith(userDir)) {
            return res.status(403).json({ error: 'Access denied' });
        }
        
        // Check if file exists
        await fs.access(resolvedPath, fs.constants.R_OK);
        
        // Serve file securely
        res.sendFile(resolvedPath, {
            headers: {
                'Content-Disposition': `attachment; filename="${filename}"`,
                'X-Content-Type-Options': 'nosniff'
            }
        });
        
        // Log file access
        console.log(`User ${userId} downloaded file: ${filename}`);
        
    } catch (error) {
        if (error.code === 'ENOENT' || error.code === 'EACCES') {
            res.status(404).json({ error: 'File not found' });
        } else {
            console.error('Error serving file:', error);
            res.status(500).json({ error: 'Internal server error' });
        }
    }
});

function isSecureFilename(filename) {
    if (typeof filename !== 'string' || filename.length === 0) {
        return false;
    }
    
    // Reject path traversal attempts
    if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
        return false;
    }
    
    // Reject hidden files and system files
    if (filename.startsWith('.') || filename.toLowerCase().includes('system')) {
        return false;
    }
    
    // Check allowed extensions
    const ext = path.extname(filename).toLowerCase();
    return ALLOWED_FILE_TYPES.includes(ext);
}

function authenticateUser(req, res, next) {
    if (!req.session || !req.session.userId) {
        return res.status(401).json({ error: 'Authentication required' });
    }
    
    req.user = {
        id: req.session.userId,
        role: req.session.userRole || 'user'
    };
    
    next();
}

function authenticateAdmin(req, res, next) {
    authenticateUser(req, res, (err) => {
        if (err) return next(err);
        
        if (req.user.role !== 'admin') {
            return res.status(403).json({ error: 'Admin access required' });
        }
        
        next();
    });
}

function getFileUploadDate(filename) {
    // Extract upload date from filename or database
    // This is a placeholder implementation
    const match = filename.match(/(\d{4}-\d{2}-\d{2})/);
    return match ? match[1] : 'Unknown';
}

💡 Why This Fix Works

The vulnerable version exposes sensitive directories through serve-index without restrictions. The secure version implements controlled access, authentication, and proper file serving without directory listings.

Why it happens

Using serve-index middleware on directories containing sensitive files without proper access controls or filtering.

Root causes

Unrestricted serve-index Usage

Using serve-index middleware on directories containing sensitive files without proper access controls or filtering.

Preview example – JAVASCRIPT
const express = require('express');
const serveIndex = require('serve-index');
const app = express();

// Vulnerable: Exposing entire directory structure
app.use('/files', express.static('uploads'), serveIndex('uploads', {'icons': true}));

// This exposes all files in uploads/ directory:
// - Configuration files
// - User uploaded files
// - Backup files
// - Log files
// - Source code

Production Directory Listing

Accidentally leaving directory listing enabled in production environments where it should be disabled.

Preview example – JAVASCRIPT
const express = require('express');
const serveIndex = require('serve-index');
const app = express();

// Vulnerable: Directory listing enabled in production
if (process.env.NODE_ENV !== 'production') {
    // Only intended for development but condition is wrong
    app.use('/static', express.static('public'), serveIndex('public'));
} else {
    // This branch should disable directory listing but doesn't
    app.use('/static', express.static('public'), serveIndex('public'));
}

Fixes

1

Remove serve-index from Production

Disable directory listing in production and limit it to development environments with proper restrictions.

2

Implement Secure File Serving

Create controlled file access endpoints instead of exposing directory listings.

Detect This Vulnerability in Your Code

Sourcery automatically identifies information disclosure from enabled directory listing via serve-index in express and many other security issues in your codebase.