Exposure of Information Through Directory Listing
Description
Exposure of Information Through Directory Listing is a vulnerability where a directory listing is exposed that reveals information about the web application or provides access to files that should not be publicly accessible. When web servers are configured to display directory contents when no default index file exists, attackers can browse the directory structure, discover hidden files, access backup files, view configuration files, and understand the application architecture. This information facilitates further attacks by revealing file names, structures, and potentially sensitive data.
Risk
Directory listing exposure poses significant security risks. Attackers can discover sensitive files such as configuration files, backup files, database dumps, log files, and source code. File naming patterns reveal application structure and potentially version information. Hidden administrative interfaces or development tools may be exposed. Backup files (file.php.bak, file.php~) often contain source code or sensitive data. Temporary files and upload directories may contain user data or exploitable content. Combined with other vulnerabilities, directory information enables targeted attacks on specific files or application components.
Solution
Disable directory listing in web server configuration globally and per directory. Use explicit index files (index.html, index.php) in all directories. Configure web servers to return 403 Forbidden or 404 Not Found for directories without index files. Remove or restrict access to backup files, temporary files, and development artifacts. Place sensitive files outside the web root. Implement proper access controls on file directories. Use web application firewalls to block directory traversal attempts. Regularly scan web servers for exposed directories using security tools.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Confidentiality Read Application Data - Directory listings expose file names, structures, and potentially file contents, revealing sensitive application information and data to attackers. |
| Confidentiality | Scope: Confidentiality Read Files or Directories - Attackers can discover and access files that were not intended to be public, including backup files, configuration files, and source code. |
Example Code
Vulnerable Code
# Vulnerable: Apache configuration with directory listing enabled
# /etc/apache2/sites-available/example.conf
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/html
# Vulnerable: Indexes option enables directory listing
<Directory /var/www/html>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# Vulnerable: No restrictions on sensitive directories
<Directory /var/www/html/uploads>
Options Indexes
# Anyone can browse uploaded files
</Directory>
# Vulnerable: Backup directory exposed
<Directory /var/www/html/backup>
Options Indexes
# Database dumps, config backups visible
</Directory>
</VirtualHost>
# Directory structure exposed:
# /var/www/html/
# ├── index.php
# ├── config.php
# ├── config.php.bak <- Source code exposed!
# ├── .htaccess
# ├── uploads/
# │ ├── user_files/
# │ └── temp/
# ├── backup/
# │ ├── db_dump.sql <- Database exposed!
# │ └── config_old.php
# └── admin/ <- Admin panel discovered!
# Vulnerable: Nginx configuration with autoindex enabled
# /etc/nginx/sites-available/example.conf
server {
listen 80;
server_name example.com;
root /var/www/html;
# Vulnerable: Global autoindex enabled
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
location / {
try_files $uri $uri/ =404;
# Directory listing if no index file
}
# Vulnerable: Upload directory browsable
location /uploads/ {
autoindex on;
# Lists all uploaded files
}
# Vulnerable: No protection for sensitive paths
location /logs/ {
autoindex on;
# Application logs exposed
}
location /includes/ {
autoindex on;
# PHP includes visible
}
}
# Vulnerable: Python web server with directory listing
from http.server import HTTPServer, SimpleHTTPRequestHandler
import os
class VulnerableHandler(SimpleHTTPRequestHandler):
# Vulnerable: Uses default directory listing behavior
def do_GET(self):
# Vulnerable: No restrictions on paths
path = self.translate_path(self.path)
if os.path.isdir(path):
# Vulnerable: Lists directory contents
return self.list_directory(path)
return super().do_GET()
# Vulnerable: Serves entire directory including sensitive files
# /app/
# ├── server.py
# ├── config.py <- Contains secrets
# ├── database.sqlite <- Full database
# ├── .env <- Environment variables
# └── users/
# └── private_data/
if __name__ == '__main__':
server = HTTPServer(('0.0.0.0', 8000), VulnerableHandler)
server.serve_forever()
// Vulnerable: Express.js with directory listing
const express = require('express');
const serveIndex = require('serve-index');
const path = require('path');
const app = express();
// Vulnerable: serve-index enables directory listing
app.use('/public', express.static('public'), serveIndex('public'));
// Vulnerable: Upload directory browsable
app.use('/uploads', express.static('uploads'), serveIndex('uploads', {
icons: true,
view: 'details' // Shows file sizes, dates
}));
// Vulnerable: No restrictions on static files
app.use(express.static('.', {
dotfiles: 'allow' // Exposes .env, .git, etc.
}));
// Directory structure exposed:
// /app/
// ├── server.js
// ├── .env <- Secrets exposed!
// ├── .git/ <- Git history exposed!
// ├── node_modules/
// ├── uploads/
// │ └── user_files/
// └── config/
// └── database.json <- Credentials exposed!
app.listen(3000);
<?php
// Vulnerable: PHP application without proper file restrictions
// Vulnerable: Directory listing via PHP
function listDirectory($path) {
// Vulnerable: No access control, lists all files
$files = scandir($path);
echo "<h2>Directory: $path</h2>";
echo "<ul>";
foreach ($files as $file) {
if ($file != '.' && $file != '..') {
$fullPath = $path . '/' . $file;
// Vulnerable: Exposes all file types
if (is_dir($fullPath)) {
echo "<li><a href='?dir=$fullPath'>$file/</a></li>";
} else {
// Vulnerable: Allows download of any file
echo "<li><a href='$fullPath'>$file</a></li>";
}
}
}
echo "</ul>";
}
// Vulnerable: Path traversal possible
$dir = $_GET['dir'] ?? '/var/www/html/uploads';
listDirectory($dir); // Can list ../../../../etc/
// Vulnerable: .htaccess not protecting these directories
// uploads/.htaccess doesn't exist or is misconfigured
// backup/.htaccess doesn't exist
?>
Fixed Code
# Fixed: Apache configuration without directory listing
# /etc/apache2/sites-available/example.conf
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/html
# Fixed: Disable directory listing globally
<Directory /var/www/html>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# Fixed: Block access to sensitive file patterns
<FilesMatch "\.(bak|old|orig|save|swp|tmp|~)$">
Require all denied
</FilesMatch>
# Fixed: Block hidden files and directories
<FilesMatch "^\.">
Require all denied
</FilesMatch>
# Fixed: Protect backup directory
<Directory /var/www/html/backup>
Require all denied
</Directory>
# Fixed: Restrict upload directory
<Directory /var/www/html/uploads>
Options -Indexes -ExecCGI
AllowOverride None
# Only allow specific file types
<FilesMatch "\.(jpg|jpeg|png|gif|pdf)$">
Require all granted
</FilesMatch>
<FilesMatch "\.php$">
Require all denied
</FilesMatch>
</Directory>
# Fixed: Deny access to sensitive paths
<DirectoryMatch "/(\.git|\.svn|\.hg|vendor|node_modules)">
Require all denied
</DirectoryMatch>
# Fixed: Block config files
<FilesMatch "(config|database|settings)\.(php|json|ini|yml)$">
Require all denied
</FilesMatch>
</VirtualHost>
# Fixed: Nginx configuration without directory listing
# /etc/nginx/sites-available/example.conf
server {
listen 80;
server_name example.com;
root /var/www/html;
# Fixed: Disable autoindex globally
autoindex off;
location / {
index index.html index.php;
try_files $uri $uri/ =404;
}
# Fixed: Protect uploads directory
location /uploads/ {
autoindex off;
# Only serve allowed file types
location ~* \.(jpg|jpeg|png|gif|pdf)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# Block PHP execution
location ~* \.php$ {
deny all;
}
# Return 404 for everything else
return 404;
}
# Fixed: Block sensitive files and directories
location ~ /\. {
deny all;
return 404;
}
location ~ \.(bak|old|orig|save|swp|tmp|sql|log)$ {
deny all;
return 404;
}
location ~ /(config|database|settings)\.(php|json|ini|yml)$ {
deny all;
return 404;
}
# Fixed: Block version control and package directories
location ~ /(\.git|\.svn|\.hg|vendor|node_modules)/ {
deny all;
return 404;
}
# Fixed: Block backup directory entirely
location /backup/ {
deny all;
return 404;
}
# Fixed: Return proper error for missing directories
location ~ ^/([^/]+)/$ {
try_files $uri $uri/index.html $uri/index.php =404;
}
}
# Fixed: Python web server with restricted directory access
from http.server import HTTPServer, BaseHTTPRequestHandler
import os
import mimetypes
class SecureHandler(BaseHTTPRequestHandler):
# Fixed: Define allowed directories and file types
ALLOWED_EXTENSIONS = {'.html', '.css', '.js', '.png', '.jpg', '.gif', '.pdf'}
WEB_ROOT = '/var/www/html/public'
BLOCKED_PATTERNS = ['.bak', '.old', '.sql', '.log', '.env', '.git']
def do_GET(self):
# Fixed: Normalize and validate path
path = self.translate_path(self.path)
# Fixed: Ensure path is within web root
real_path = os.path.realpath(path)
if not real_path.startswith(os.path.realpath(self.WEB_ROOT)):
self.send_error(403, 'Forbidden')
return
# Fixed: Block sensitive file patterns
if any(pattern in real_path.lower() for pattern in self.BLOCKED_PATTERNS):
self.send_error(404, 'Not Found')
return
# Fixed: No directory listing - return 403 for directories
if os.path.isdir(real_path):
index_path = os.path.join(real_path, 'index.html')
if os.path.exists(index_path):
real_path = index_path
else:
self.send_error(403, 'Directory listing not allowed')
return
# Fixed: Check file extension
ext = os.path.splitext(real_path)[1].lower()
if ext not in self.ALLOWED_EXTENSIONS:
self.send_error(403, 'File type not allowed')
return
# Serve the file
try:
with open(real_path, 'rb') as f:
content = f.read()
self.send_response(200)
mime_type = mimetypes.guess_type(real_path)[0] or 'application/octet-stream'
self.send_header('Content-Type', mime_type)
self.send_header('Content-Length', len(content))
self.end_headers()
self.wfile.write(content)
except FileNotFoundError:
self.send_error(404, 'Not Found')
def translate_path(self, path):
# Fixed: Safely translate path within web root
path = path.split('?')[0].split('#')[0]
path = os.path.normpath(path).lstrip('/')
return os.path.join(self.WEB_ROOT, path)
if __name__ == '__main__':
server = HTTPServer(('0.0.0.0', 8000), SecureHandler)
server.serve_forever()
// Fixed: Express.js without directory listing
const express = require('express');
const path = require('path');
const app = express();
// Fixed: Define allowed file extensions
const ALLOWED_EXTENSIONS = ['.html', '.css', '.js', '.png', '.jpg', '.gif', '.pdf'];
const BLOCKED_PATTERNS = ['.bak', '.old', '.sql', '.log', '.env', '.git', 'config'];
// Fixed: Custom static file middleware with restrictions
function secureStatic(root, options = {}) {
return (req, res, next) => {
const requestedPath = path.normalize(decodeURIComponent(req.path));
const fullPath = path.join(root, requestedPath);
const realRoot = path.resolve(root);
// Fixed: Prevent path traversal
if (!fullPath.startsWith(realRoot)) {
return res.status(403).send('Forbidden');
}
// Fixed: Block sensitive patterns
if (BLOCKED_PATTERNS.some(p => requestedPath.toLowerCase().includes(p))) {
return res.status(404).send('Not Found');
}
// Fixed: Check file extension
const ext = path.extname(requestedPath).toLowerCase();
if (ext && !ALLOWED_EXTENSIONS.includes(ext)) {
return res.status(403).send('File type not allowed');
}
// Fixed: Block dotfiles
if (requestedPath.split('/').some(part => part.startsWith('.'))) {
return res.status(404).send('Not Found');
}
next();
};
}
// Fixed: Secure static file serving (no serve-index)
app.use('/public',
secureStatic(path.join(__dirname, 'public')),
express.static(path.join(__dirname, 'public'), {
dotfiles: 'deny',
index: ['index.html']
})
);
// Fixed: Uploads with strict controls
app.use('/uploads',
secureStatic(path.join(__dirname, 'uploads')),
express.static(path.join(__dirname, 'uploads'), {
dotfiles: 'deny',
index: false, // No index files served
extensions: ['jpg', 'png', 'gif', 'pdf'] // Only these types
})
);
// Fixed: Return 404 for directory requests without index
app.use((req, res, next) => {
if (req.path.endsWith('/') && req.path !== '/') {
return res.status(404).send('Not Found');
}
next();
});
app.listen(3000);
<?php
// Fixed: PHP application with proper file access controls
class SecureFileServer {
private const WEB_ROOT = '/var/www/html/public';
private const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'html', 'css', 'js'];
private const BLOCKED_PATTERNS = ['.bak', '.old', '.sql', '.log', '.env', '.git', 'config', '.htaccess'];
public function serveFile(string $requestedPath): void {
// Fixed: Normalize and validate path
$requestedPath = $this->normalizePath($requestedPath);
$fullPath = realpath(self::WEB_ROOT . '/' . $requestedPath);
$webRoot = realpath(self::WEB_ROOT);
// Fixed: Prevent path traversal
if ($fullPath === false || strpos($fullPath, $webRoot) !== 0) {
$this->send403();
return;
}
// Fixed: Block sensitive patterns
foreach (self::BLOCKED_PATTERNS as $pattern) {
if (stripos($requestedPath, $pattern) !== false) {
$this->send404();
return;
}
}
// Fixed: No directory listing
if (is_dir($fullPath)) {
$indexFile = $fullPath . '/index.html';
if (file_exists($indexFile)) {
$fullPath = $indexFile;
} else {
$this->send403('Directory listing not allowed');
return;
}
}
// Fixed: Check file extension
$extension = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));
if (!in_array($extension, self::ALLOWED_EXTENSIONS)) {
$this->send403('File type not allowed');
return;
}
// Fixed: Serve file with proper headers
if (file_exists($fullPath) && is_readable($fullPath)) {
$mimeType = $this->getMimeType($fullPath);
header('Content-Type: ' . $mimeType);
header('Content-Length: ' . filesize($fullPath));
header('X-Content-Type-Options: nosniff');
readfile($fullPath);
} else {
$this->send404();
}
}
private function normalizePath(string $path): string {
// Remove query string and fragments
$path = strtok($path, '?#');
// Remove directory traversal attempts
$path = str_replace(['../', '..\\'], '', $path);
// Normalize slashes
$path = str_replace('\\', '/', $path);
// Remove leading slash
return ltrim($path, '/');
}
private function getMimeType(string $path): string {
$mimeTypes = [
'html' => 'text/html',
'css' => 'text/css',
'js' => 'application/javascript',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'pdf' => 'application/pdf'
];
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
return $mimeTypes[$ext] ?? 'application/octet-stream';
}
private function send403(string $message = 'Forbidden'): void {
http_response_code(403);
echo $message;
}
private function send404(): void {
http_response_code(404);
echo 'Not Found';
}
}
// Fixed: Use secure file server instead of directory listing
$server = new SecureFileServer();
$server->serveFile($_SERVER['REQUEST_URI']);
?>
CVE Examples
- CVE-2021-41773: Apache HTTP Server path traversal and directory listing vulnerability allowing file disclosure.
- CVE-2020-5410: Spring Cloud Config Server allowed directory traversal to access any file.
- CVE-2019-3799: Spring Cloud Config Server allowed applications to serve files from arbitrary URLs.
References
- MITRE Corporation. "CWE-548: Exposure of Information Through Directory Listing." https://cwe.mitre.org/data/definitions/548.html
- OWASP. "Testing for Directory Traversal/File Include."
- Apache. "Apache Module mod_autoindex - Security Considerations."