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

ImpactDetails
ConfidentialityScope: 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.


  • 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

  1. MITRE Corporation. "CWE-1230: Exposure of Sensitive Information Through Metadata." https://cwe.mitre.org/data/definitions/1230.html
  2. OWASP Data Protection Guidelines
  3. Privacy by Design Principles