Skip to content

CWE-548: Exposure of Information Through Directory Listing

Overview

A directory listing is what a server returns when a request for a directory path has no index file to serve and auto-index is enabled: an index of the directory's contents, showing file names and structure. The same exposure comes from a framework's static-file handler with its listing option turned on, or from an application route that browses a directory with no access control. Attackers read listings to find backup files, configuration files, source code and documentation that were not meant to be public.

Relationship to Other CWEs

Report a finding here when the listing itself is the exposure. Where the listing is merely how something else was noticed, the more useful mapping is whatever put that file within reach. CWE-548 sits under CWE-497 (Exposure of Sensitive System Information to an Unauthorized Control Sphere) and has no children, so there is nothing more specific to route to below it.

The pages around it differ by what the client actually got hold of:

OWASP Classification

A01:2025 - Broken Access Control

Risk

Medium: A listing gives away the application's structure and file naming conventions, and points straight at backup and temporary files (.bak, .old, ~), configuration files, source code and documentation. It is not exploitable on its own, but it tells an attacker where to look next.

Remediation Steps

Core Principle: Disable directory browsing on the web server and on every application framework that serves static files. An index file in each public directory is a backstop, not the fix.

Disable Directory Listing in Web Server

Apache:

# In httpd.conf or the site's vhost - target the block that matches your real document root
<Directory /var/www/html>
    Options -Indexes
</Directory>

Put the directive in a <Directory> block for the actual docroot rather than in <Directory />. Debian and Ubuntu ship <Directory /var/www/> Options Indexes FollowSymLinks</Directory> in apache2.conf, and that more specific block wins over anything set on /, so a "global disable" at the root is silently overridden.

The .htaccess route works only if the enclosing <Directory> block allows it. Debian and Ubuntu set AllowOverride None on /var/www/ by default, so an .htaccess file is never read and the fix appears to apply while changing nothing; if AllowOverride is set but does not include Options, Apache returns 500 instead. To use .htaccess, set AllowOverride Options (or AllowOverride All) on that directory first - and if you can edit the server config to do that, set Options -Indexes there directly instead.

Nginx:

# In nginx.conf or site config
autoindex off;

# Ensure in all location blocks
location / {
    autoindex off;
}

IIS:

<!-- In web.config -->
<configuration>
    <system.webServer>
        <directoryBrowse enabled="false" />
    </system.webServer>
</configuration>

Don't Substitute Placeholder Index Files for the Server Setting

Dropping an empty index.html into every directory gives the server something to serve instead of a listing, but it leaves auto-index enabled. The exposure returns the moment a directory appears without one - a new upload folder, or a path a deploy script forgot to touch. It also does nothing about non-standard access paths such as WebDAV PROPFIND, which some servers use to enumerate a directory without going through normal index resolution. Treat a placeholder index file as a stopgap for a directory you can't immediately reconfigure, not as a replacement for disabling auto-index at the server level above.

Configure Application Frameworks

A framework can reintroduce directory listing even when the web server's own auto-index is off. A static-file middleware with its listing option enabled, or a hand-written route that lists a directory and renders the result, produces the same exposure at the application layer. Serve static files through the framework's plain static-file handler with no listing option enabled, rather than through a dedicated directory-browsing package or middleware.

// VULNERABLE - a route hand-rolls directory listing with no access control
route('/files/', () => {
    files = list_directory('/var/www/files')
    return render('files.html', files)
})

// SECURE - remove the listing endpoint entirely, or gate it behind authorization
route('/files/', require_authentication, () => {
    if not current_user.is_admin:
        return 403
    files = get_user_files(current_user)   // scoped to what this user is allowed to see
    return render('files.html', files)
})

Turn listing off on whichever component actually serves the directory. No layer above it can suppress a listing the origin has already generated: a CDN in front of the site has no listing directive of its own, so an origin that returns a listing gets that listing served and cached. Check the setting on the component itself:

  • nginx: autoindex - off by default; confirm nothing has switched it on.
  • Apache: Options -Indexes on the <Directory> block for the docroot, as above.
  • IIS: <directoryBrowse enabled="false" /> - directory browsing is disabled by default.
  • Tomcat: the listings init-param on DefaultServlet in conf/web.xml - defaults to false.
  • Express: express.static serves named files only and returns 404 for a directory path - it has no listing option at all. A listing appears only where the separate serve-index package is mounted, so the fix there is to remove that middleware.

Remove Sensitive Files from Webroot

Don't store these in publicly accessible directories:

  • Backup files: *.bak, *.old, *~
  • Configuration: .env, config.php, web.config
  • Source code: .git/, .svn/, *.py, *.rb
  • Documentation: README.md, INSTALL.txt
  • Database files: *.db, *.sqlite

Test for Directory Listing

Assert on the response body, not the status code. There is no single correct status: Apache and nginx return 403 once Options -Indexes/autoindex off is in place, express.static returns 404 for a directory path, a directory that has an index file correctly returns 200, and a redirect to a canonical path is also fine. What all of those have in common is that the body is not a generated listing.

# Every directory path should fail this grep. A hit is a listing.
for path in /uploads/ /images/ /css/ /js/; do
    if curl -sL "http://example.com${path}" | grep -qi "<title>Index of\|<h1>Index of"; then
        echo "LISTING SERVED: ${path}"
    fi
done

Index of in the <title> and <h1> is what both mod_autoindex and nginx emit; a framework-generated listing will use its own markup, so check the specific package's output when the listing came from the application layer rather than the web server.

Monitor and Alert

Set up monitoring for:

  • A directory path returning a generated listing body. Alert on the listing markup rather than the status code, for the reasons given under Test for Directory Listing above
  • Scanners walking the directory structure, and request patterns that look like enumeration

Additional Resources