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
- CWE-538 (this page) - the application writing sensitive data into a file or directory that external parties can reach
- CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor) - the parent, and the router page to use if the finding is better described by a different sibling
- CWE-532 (Insertion of Sensitive Information into Log File) - this page's child, the specific case where the externally-accessible file is a log
- CWE-615 (Inclusion of Sensitive Information in Source Code Comments) - reached through CWE-540 (Inclusion of Sensitive Information in Source Code), a child of this page with no page here; CWE-615 covers the case where the data sits in a source comment
- CWE-530 (Exposure of Backup File to an Unauthorized Control Sphere) - the near-twin by subject, reaching the corpus down a different branch. MITRE records no relationship between the two, and the split is how the file got there: this page is the application writing sensitive data to a reachable location, CWE-530 is a copy of something legitimate left behind where it can be fetched
- MITRE also lists CWE-651 (Exposure of WSDL File Containing Sensitive Information) as a further child of this page, with no page here
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:
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:
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:
Apache:
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
- CWE-538: Insertion of Sensitive Information into Externally-Accessible File or Directory
- OWASP Secrets Management Cheat Sheet - where credentials belong instead of a served directory
- OWASP Testing Guide - Review Old Backup and Unreferenced Files
- OWASP Testing Guide - Test File Extensions Handling
- OWASP Top 10 2025 A01: Broken Access Control