Exposure of Sensitive Information Through Metadata
Description
Exposure of Sensitive Information Through Metadata occurs when a product prevents direct access to a resource containing sensitive information, but does not sufficiently limit access to metadata that is derived from the original, sensitive information. Developers often secure primary data sources while overlooking metadata repositories. Portions of the original information might be recorded in metadata, search indices, statistical reports, or other resources. Attackers can exploit this by using targeted search queries or analyzing metadata patterns like creation dates and activity logs to infer sensitive details about individuals or systems.
Risk
Metadata exposure has significant security implications. Sensitive information may be inferred from metadata patterns. Search indices may expose protected data. Statistical reports may reveal private details. Creation dates and modification times may leak activity. Document properties may contain confidential information. Log files may expose user behavior. Aggregated metadata may enable de-anonymization. Access patterns may reveal business intelligence.
Solution
Identify all metadata derived from sensitive data. Apply same access controls to metadata as primary data. Sanitize metadata before storage or transmission. Limit search index access to authorized users. Review statistical reports for information leakage. Strip document metadata before sharing. Implement metadata access logging and monitoring. Consider metadata when designing access control policies. Regularly audit metadata repositories for sensitive content.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Confidentiality Read Application Data - Attackers gain unauthorized access to sensitive application information through metadata analysis. |
Example Code
Vulnerable Code
# Vulnerable: Document system exposes metadata
import os
import json
from datetime import datetime
class VulnerableDocumentSystem:
def __init__(self):
self.documents = {} # id -> document content
self.metadata = {} # id -> metadata (publicly accessible!)
def upload_document(self, doc_id, content, author, classification):
# Store document with access control
self.documents[doc_id] = {
'content': content,
'classification': classification,
'access_control': ['authorized_users_only']
}
# VULNERABLE: Metadata stored separately without access control
self.metadata[doc_id] = {
'author': author,
'created': datetime.now().isoformat(),
'modified': datetime.now().isoformat(),
'size': len(content),
'classification': classification, # Leaks sensitivity level!
'keywords': self.extract_keywords(content) # Leaks content!
}
def get_document(self, doc_id, user):
# Proper access control for document content
doc = self.documents.get(doc_id)
if doc and self.check_access(user, doc['access_control']):
return doc['content']
return None
def get_metadata(self, doc_id, user):
# VULNERABLE: No access control on metadata!
# Anyone can see metadata including classification and keywords
return self.metadata.get(doc_id)
def search(self, query, user):
# VULNERABLE: Search reveals existence of protected documents
results = []
for doc_id, meta in self.metadata.items():
if query.lower() in str(meta.get('keywords', [])).lower():
results.append({
'id': doc_id,
'author': meta['author'],
'classification': meta['classification']
})
return results # Returns results user cannot access!
// Vulnerable: File metadata exposure
import java.io.File;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
public class VulnerableFileService {
private static final String SECURE_FOLDER = "/secure/documents/";
// Document content is protected
public byte[] getDocument(String filename, User user) {
if (!hasAccess(user, filename)) {
throw new SecurityException("Access denied");
}
return Files.readAllBytes(Paths.get(SECURE_FOLDER + filename));
}
// VULNERABLE: Metadata exposed without access control
public FileMetadata getFileMetadata(String filename) {
File file = new File(SECURE_FOLDER + filename);
BasicFileAttributes attrs = Files.readAttributes(
file.toPath(), BasicFileAttributes.class);
return new FileMetadata(
file.getName(),
attrs.size(), // Reveals file exists and size
attrs.creationTime(), // Reveals when created
attrs.lastModifiedTime() // Reveals activity patterns
);
}
// VULNERABLE: Directory listing exposes protected files
public List<String> listFiles(String folder) {
File dir = new File(SECURE_FOLDER + folder);
return Arrays.asList(dir.list()); // Lists all files!
}
}
// Vulnerable: API exposes sensitive metadata
class VulnerableUserAPI {
constructor(database) {
this.db = database;
}
// User profile is protected
async getUserProfile(userId, requestingUser) {
if (!this.canViewProfile(requestingUser, userId)) {
throw new Error('Access denied');
}
return this.db.getUser(userId);
}
// VULNERABLE: Statistics expose user existence and activity
async getUserStats(userId) {
const user = await this.db.getUser(userId);
if (!user) {
return null;
}
// Reveals sensitive information about protected user
return {
accountCreated: user.createdAt,
lastActive: user.lastLogin,
postCount: user.posts.length,
followerCount: user.followers.length,
// Even reveals if account is private!
isPrivate: user.isPrivate
};
}
// VULNERABLE: Search reveals protected users
async searchUsers(query) {
const users = await this.db.searchUsers(query);
return users.map(u => ({
id: u.id,
username: u.username,
joinDate: u.createdAt // Metadata about all users
}));
}
}
Fixed Code
# Fixed: Document system with metadata protection
import os
import json
from datetime import datetime
class SecureDocumentSystem:
def __init__(self):
self.documents = {}
def upload_document(self, doc_id, content, author, classification):
# Store document and metadata together with same access control
self.documents[doc_id] = {
'content': content,
'metadata': {
'author': author,
'created': datetime.now().isoformat(),
'modified': datetime.now().isoformat(),
'size': len(content),
'classification': classification,
'keywords': self.extract_keywords(content)
},
'access_control': self.get_access_list(classification)
}
def get_document(self, doc_id, user):
doc = self.documents.get(doc_id)
if doc and self.check_access(user, doc['access_control']):
return doc['content']
return None
def get_metadata(self, doc_id, user):
# Same access control for metadata
doc = self.documents.get(doc_id)
if doc and self.check_access(user, doc['access_control']):
return doc['metadata']
return None # Don't reveal document exists
def search(self, query, user):
# Only return documents user can access
results = []
for doc_id, doc in self.documents.items():
if self.check_access(user, doc['access_control']):
if query.lower() in str(doc['metadata'].get('keywords', [])).lower():
results.append({
'id': doc_id,
'author': doc['metadata']['author'],
# Only show classification if user has clearance
'classification': doc['metadata']['classification']
if self.can_see_classification(user) else None
})
return results
def get_sanitized_metadata(self, doc_id, user):
# Return minimal metadata for public queries
doc = self.documents.get(doc_id)
if not doc:
return None
if self.check_access(user, doc['access_control']):
return doc['metadata']
# For unauthorized users, return minimal safe info
return {
'exists': True,
'accessible': False
# Don't reveal size, dates, classification, etc.
}
// Fixed: File service with metadata access control
import java.io.File;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
public class SecureFileService {
private static final String SECURE_FOLDER = "/secure/documents/";
public byte[] getDocument(String filename, User user) {
if (!hasAccess(user, filename)) {
throw new SecurityException("Access denied");
}
return Files.readAllBytes(Paths.get(SECURE_FOLDER + filename));
}
// Fixed: Apply same access control to metadata
public FileMetadata getFileMetadata(String filename, User user) {
if (!hasAccess(user, filename)) {
throw new SecurityException("Access denied");
}
File file = new File(SECURE_FOLDER + filename);
BasicFileAttributes attrs = Files.readAttributes(
file.toPath(), BasicFileAttributes.class);
return new FileMetadata(
file.getName(),
attrs.size(),
attrs.creationTime(),
attrs.lastModifiedTime()
);
}
// Fixed: Only list files user can access
public List<String> listFiles(String folder, User user) {
File dir = new File(SECURE_FOLDER + folder);
String[] allFiles = dir.list();
// Filter to only accessible files
List<String> accessibleFiles = new ArrayList<>();
for (String filename : allFiles) {
if (hasAccess(user, folder + "/" + filename)) {
accessibleFiles.add(filename);
}
}
return accessibleFiles;
}
// Sanitize metadata for external sharing
public FileMetadata sanitizeForExport(FileMetadata meta) {
return new FileMetadata(
meta.getName(),
0, // Don't reveal size
null, // Don't reveal creation time
null // Don't reveal modification time
);
}
}
// Fixed: API with protected metadata
class SecureUserAPI {
constructor(database) {
this.db = database;
}
async getUserProfile(userId, requestingUser) {
if (!this.canViewProfile(requestingUser, userId)) {
throw new Error('Access denied');
}
return this.db.getUser(userId);
}
// Fixed: Apply same access control to statistics
async getUserStats(userId, requestingUser) {
// Must have access to view stats
if (!this.canViewProfile(requestingUser, userId)) {
// Don't reveal user existence
throw new Error('Not found');
}
const user = await this.db.getUser(userId);
if (!user) {
throw new Error('Not found');
}
// Return stats based on what requester can see
const stats = {
username: user.username
};
if (this.canViewActivity(requestingUser, userId)) {
stats.lastActive = user.lastLogin;
stats.postCount = user.posts.length;
}
// Don't expose private account status
return stats;
}
// Fixed: Search only returns accessible users
async searchUsers(query, requestingUser) {
const users = await this.db.searchUsers(query);
// Filter to accessible users
const accessibleUsers = users.filter(u =>
this.canViewInSearch(requestingUser, u)
);
// Return minimal metadata
return accessibleUsers.map(u => ({
id: u.id,
username: u.username
// Don't include joinDate or other metadata
}));
}
// Rate-limit search to prevent enumeration
async rateLimitedSearch(query, requestingUser) {
await this.checkRateLimit(requestingUser, 'search');
return this.searchUsers(query, requestingUser);
}
}
CVE Examples
Metadata exposure vulnerabilities have been found in document management systems, file sharing platforms, and social media applications where protected content could be discovered through metadata queries.
Related CWEs
- CWE-285: Improper Authorization (parent)
- CWE-202: Exposure of Sensitive Information Through Data Queries (child)
- CWE-612: Improper Authorization of Index Containing Sensitive Information (child)
References
- MITRE Corporation. "CWE-1230: Exposure of Sensitive Information Through Metadata." https://cwe.mitre.org/data/definitions/1230.html
- OWASP Data Protection Guidelines
- Privacy by Design Principles