Skip to content

CWE-538: Insertion of Sensitive Information into Externally-Accessible File or Directory

Overview

Insertion of sensitive information into an externally-accessible file or directory occurs when applications place sensitive data - file system metadata, backup content, credentials, or internal structure - somewhere a client outside the intended trust boundary can reach it: a directory listing, a predictably-named backup file, a static asset served alongside application files, or file metadata exposed in HTTP headers. Some of what this exposes is sensitive in its own right - a backup file holding real data, an image whose EXIF still carries GPS coordinates. The rest is a map: which paths exist, how files are named, and where a traversal or enumeration attempt is worth aiming.

Relationship to Other CWEs

OWASP Classification

A01:2025 - Broken Access Control

Risk

Low to High: A directory listing or a path-bearing error message discloses which files exist and how they are named, which is what makes enumeration worth attempting - on its own that is Low. It rises with what the exposure actually turns up: the backup and temporary files it reveals hold real data, and a reachable file carrying credentials is High whatever the listing itself was worth. Paired with another flaw, the same information turns guesswork into a targeted attempt.

Remediation Steps

Core Principle: Never reveal internal file system paths, directory structures, or file metadata to untrusted users; use opaque identifiers and generic error messages.

Disable Directory Listings

Apache:

<Directory /var/www/html>
    Options -Indexes
</Directory>

Put this in httpd.conf or the vhost, against the block that matches your real document root. An .htaccess file is not read at all under AllowOverride None, which is what Debian and Ubuntu default to, and setting Options from one additionally requires AllowOverride Options or All - without it Apache returns a 500 rather than ignoring the line. See CWE-548 for the full directory-listing case.

Nginx:

autoindex off;

Static file serving at the application layer needs the same treatment: configure it to serve named files only, not directory contents.

Remove Path Information from Errors

// VULNERABLE - exposes the full internal path
try:
    data = read_json(base_dir + '/' + file_id + '.json')
except FileNotFoundError as e:
    return e.message, 404   // shows the full path, e.g. "/var/www/app/data/12345.json not found"

// SECURE - generic error message, detail logged server-side
try:
    data = read_json(safe_file_path(file_id))
except FileNotFoundError:
    log.error('file not found', file_id)
    return { error: 'Resource not found' }, 404

Use Opaque File References

// VULNERABLE - the identifier the client holds is sequential, so an attacker walks it
file_path = '/uploads/' + user_id + '/document_' + doc_id + '.pdf'
route('/files/{doc_id}', () => send_file(path_for(doc_id)))

// SECURE - the opaque ID is the client-facing handle; the real path is never exposed
storage_id = generate_uuid()
db.save_file_mapping(storage_id = storage_id, owner_id = user_id, storage_path = internal_path)

// retrieval keys on the opaque ID the client actually holds, and authorizes before serving
route('/files/{storage_id}', () => {
    record = db.get_file_by_storage_id(storage_id)
    if record is null or not current_user.can_access(record):
        return { error: 'Resource not found' }, 404
    return send_file(record.storage_path)
})

An opaque handle makes guessing impractical, but it is not what stops enumeration - randomizing the storage path never did, because an attacker increments whichever identifier the client is given, not the one on disk. The enumeration control is the per-request authorization check, which returns the same 404 whether the record is missing or simply not this user's (see Implement Consistent File Access below).

Strip File Metadata From Downloads

// SECURE - strip what is embedded in the file, then send it under a generic name
function download(file_id):
    filepath = get_secure_path(file_id)
    clean = strip_metadata(filepath)   // EXIF/XMP for images, document properties for PDF and Office
    return send_as_attachment(clean, download_name = 'document.pdf', content_type = 'application/pdf')
    // download_name is a fixed, generic name - not the internal storage path or original filename

A generic download_name hides the filename, not the contents. Metadata inside the file passes through untouched: user-uploaded images carry EXIF with GPS coordinates and camera serial numbers, PDFs carry Author, Producer and frequently the full filesystem path of the source document, and Office files carry author, company and revision history. The strip_metadata step needs a real implementation - ExifTool (exiftool -all= file.jpg) handles most formats, piexif or Pillow cover JPEG and TIFF in Python, and pikepdf clears PDF document info and XMP. Stripping once at upload time is cheaper than on every download, as long as no later processing step re-adds it.

Protect Backup and Temporary Files

# Nginx - deny access to backup files
location ~* \.(bak|old|orig|save|swo|swp|tmp|temp)$ {
    deny all;
}

# Deny common editor backup patterns  
location ~ ~$ {
    deny all;
}

Implement Consistent File Access

// VULNERABLE - different messages let a caller distinguish "doesn't exist" from "exists but I can't see it"
if not exists(filepath):
    return 'File not found', 404
if not user.can_access(filepath):
    return 'Access denied', 403

// SECURE - identical response for both cases
filepath = get_file_path(file_id)
if filepath is null or not user.can_access(file_id):
    return 'Resource not found', 404   // same message either way

Remove Server Version Headers (Defense in Depth)

This one does not close the weakness. CWE-538 is a sensitive file or directory being reachable where it should not be, and a Server header does not make anything reachable - it tells an attacker which version-specific paths are worth trying against the files you have already removed or protected above. Do it, but after the sections above, and do not treat a finding as fixed because the header is gone.

Nginx:

server_tokens off;

Apache:

ServerTokens Prod
ServerSignature Off

Disable equivalent identifying headers at the application/framework layer too (e.g. Express's X-Powered-By) - the web server setting alone doesn't cover headers the application framework adds on its own. CWE-497 covers version and environment disclosure in its own right.

Additional Resources