Unprotected File Storage
ID |
swift.unprotected_file_storage |
Severity |
high |
Resource |
Information leak |
Language |
Swift |
Tags |
CWE:311, CWE:312, CWE:359, MASVS:storage-1, MASWE:0006, MASWE:0007, NIST.SP.800-53, OWASP:2021:A2, PCI-DSS:3.4 |
Description
iOS provides data protection classes that control when files can be accessed based on the device lock state. Using .noFileProtection (also known as NSFileProtectionNone) creates files that are always accessible, even when the device is locked. This represents a critical security vulnerability for any application storing sensitive data.
When a file is created without protection, iOS does not encrypt its contents with the user’s passcode-derived keys. This means that an attacker with physical access to a locked device, or forensic tools operating on a device image, can extract and read these files without needing to unlock the device.
Weak file protection attributes include:
-
.noFileProtection/NSFileProtectionNone- File content is not encrypted and remains accessible regardless of device lock state. This provides no data protection whatsoever.
Rationale
The following example demonstrates vulnerable code that creates files without adequate protection:
import Foundation
func saveUserCredentials(username: String, password: String) {
let credentials = "\(username):\(password)"
// VULNERABLE: Using .noFileProtection
let fileURL = getDocumentsDirectory().appendingPathComponent("credentials.txt")
do {
try credentials.write(
to: fileURL,
atomically: true,
encoding: .utf8
)
// Setting no file protection after creation
try FileManager.default.setAttributes(
[.protectionKey: FileProtectionType.noFileProtection], // WEAK!
ofItemAtPath: fileURL.path
)
} catch {
print("Error saving credentials: \(error)")
}
}
func saveAPIToken(token: String) {
let fileURL = getDocumentsDirectory().appendingPathComponent("api_token.dat")
// VULNERABLE: Creating file with no protection
let attributes: [FileAttributeKey: Any] = [
.protectionKey: FileProtectionType.noFileProtection // WEAK!
]
FileManager.default.createFile(
atPath: fileURL.path,
contents: token.data(using: .utf8),
attributes: attributes
)
}
func storePaymentInfo(cardNumber: String, cvv: String) {
let paymentData = "Card: \(cardNumber), CVV: \(cvv)"
let fileURL = getDocumentsDirectory().appendingPathComponent("payment.dat")
do {
try paymentData.write(to: fileURL, atomically: true, encoding: .utf8)
// VULNERABLE: Explicitly removing protection
try (fileURL as NSURL).setResourceValue(
NSFileProtectionNone, // WEAK! Using NSFileProtectionNone constant
forKey: .fileProtectionKey
)
} catch {
print("Error: \(error)")
}
}
func getDocumentsDirectory() -> URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
This code has several critical security problems:
-
No encryption at rest: Files with
.noFileProtectionare not encrypted with the device’s hardware keys, leaving sensitive data in cleartext on disk. -
Accessible when locked: Even when the device is locked, these files remain readable by any process with appropriate file system access.
-
Vulnerable to forensic extraction: Law enforcement, forensic tools, or malware can extract these files from a locked device without needing the user’s passcode.
-
No hardware security: The Secure Enclave and hardware encryption features are not utilized for these files.
-
Persistent vulnerability: Once a file is created with no protection, it remains vulnerable until explicitly re-encrypted with a stronger protection class.
-
Backup exposure: Unprotected files in backups (iCloud, iTunes) are also not encrypted with the device passcode, making them accessible if backups are compromised.
Remediation
Use appropriate file protection classes that encrypt files and restrict access based on device lock state:
Option 1: .complete (FileProtectionType.complete) - Recommended for most sensitive data
This is the strongest protection level, encrypting files with keys that are only available when the device is unlocked:
import Foundation
class SecureFileStorage {
func saveUserCredentials(username: String, password: String) throws {
let credentials = "\(username):\(password)"
let fileURL = getDocumentsDirectory().appendingPathComponent("credentials.txt")
// SECURE: Using .complete for maximum protection
try credentials.write(
to: fileURL,
atomically: true,
encoding: .utf8
)
try FileManager.default.setAttributes(
[.protectionKey: FileProtectionType.complete], // Only accessible when unlocked
ofItemAtPath: fileURL.path
)
print("✅ Credentials saved with complete protection")
}
func saveAPIToken(token: String) throws {
let fileURL = getDocumentsDirectory().appendingPathComponent("api_token.dat")
// SECURE: Creating file with complete protection from the start
let attributes: [FileAttributeKey: Any] = [
.protectionKey: FileProtectionType.complete
]
FileManager.default.createFile(
atPath: fileURL.path,
contents: token.data(using: .utf8),
attributes: attributes
)
print("✅ API token saved with complete protection")
}
func storePaymentInfo(cardNumber: String, cvv: String) throws {
let paymentData = "Card: \(cardNumber), CVV: \(cvv)"
let fileURL = getDocumentsDirectory().appendingPathComponent("payment.dat")
try paymentData.write(to: fileURL, atomically: true, encoding: .utf8)
// SECURE: Using NSFileProtectionComplete constant
try (fileURL as NSURL).setResourceValue(
NSFileProtectionComplete,
forKey: .fileProtectionKey
)
print("✅ Payment info saved with complete protection")
}
private func getDocumentsDirectory() -> URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
}
Option 2: .completeUnlessOpen - For files that need to stay accessible while open
Use this when you need to keep a file accessible even after the device locks, but only if it was already open:
class DocumentStorage {
func createSecureLogFile() throws {
let logURL = getDocumentsDirectory().appendingPathComponent("app.log")
// SECURE: File remains accessible while open, even if device locks
let attributes: [FileAttributeKey: Any] = [
.protectionKey: FileProtectionType.completeUnlessOpen
]
FileManager.default.createFile(
atPath: logURL.path,
contents: Data(),
attributes: attributes
)
print("✅ Log file created with completeUnlessOpen protection")
print(" • File encrypted when closed")
print(" • Remains accessible while open, even if device locks")
}
private func getDocumentsDirectory() -> URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
}
Option 3: .completeUntilFirstUserAuthentication - For background operations
Use this for files that background processes need to access after the first device unlock:
class BackgroundDataStorage {
func saveSyncData(data: Data) throws {
let syncURL = getDocumentsDirectory().appendingPathComponent("sync_data.dat")
// SECURE: Accessible after first unlock, even if device locks again
let attributes: [FileAttributeKey: Any] = [
.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication
]
FileManager.default.createFile(
atPath: syncURL.path,
contents: data,
attributes: attributes
)
print("✅ Sync data saved with completeUntilFirstUserAuthentication")
print(" • File accessible after first unlock since boot")
print(" • Suitable for background sync operations")
}
private func getDocumentsDirectory() -> URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
}
Option 4: Setting protection for existing files
Upgrade protection on files that were created without adequate protection:
class FileProtectionUpgrade {
func upgradeFileProtection(at url: URL) throws {
// Check current protection level
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
let currentProtection = attributes[.protectionKey] as? FileProtectionType
print("Current protection: \(String(describing: currentProtection))")
// SECURE: Upgrade to complete protection
try FileManager.default.setAttributes(
[.protectionKey: FileProtectionType.complete],
ofItemAtPath: url.path
)
print("✅ File protection upgraded to .complete")
}
func upgradeDirectoryProtection(at directoryURL: URL) throws {
let fileManager = FileManager.default
let contents = try fileManager.contentsOfDirectory(
at: directoryURL,
includingPropertiesForKeys: [.isRegularFileKey],
options: .skipsHiddenFiles
)
for fileURL in contents {
let resourceValues = try fileURL.resourceValues(forKeys: [.isRegularFileKey])
if resourceValues.isRegularFile == true {
// SECURE: Upgrade each file to complete protection
try fileManager.setAttributes(
[.protectionKey: FileProtectionType.complete],
ofItemAtPath: fileURL.path
)
print("✅ Upgraded: \(fileURL.lastPathComponent)")
}
}
}
}
Comparison of File Protection Levels:
| Protection Level | When Accessible | Encryption | Use Case |
|---|---|---|---|
|
Always, even when locked ❌ |
⚠️ None |
Never use for sensitive data |
|
Only when device is unlocked ✅ |
✅ Strong |
Sensitive user data (credentials, tokens) |
|
When unlocked or already open 🟡 |
✅ Strong |
Files that must stay open across locks |
|
After first unlock since boot 🟡 |
✅ Strong |
Background sync, app launch data |
Best Practices:
-
Default to .complete: Always use
.complete(orNSFileProtectionComplete) for sensitive data like credentials, API tokens, personal information, and payment data. -
Set protection at creation: Specify file protection attributes when creating files, rather than changing them later:
FileManager.default.createFile( atPath: path, contents: data, attributes: [.protectionKey: FileProtectionType.complete] ) -
Enable Data Protection in Xcode: Ensure the "Data Protection" capability is enabled in your app’s entitlements (it’s enabled by default for new apps).
-
Handle errors gracefully: File access may fail when the device is locked and you’re using
.complete. Implement proper error handling:do { let data = try Data(contentsOf: protectedFileURL) // Process data } catch CocoaError.fileReadNoPermission { print("File not accessible - device may be locked") } catch { print("Error reading file: \(error)") } -
Audit existing files: Review all file operations in your codebase and upgrade protection levels for sensitive data.
-
Document protection levels: Clearly document which protection level is used for each file type and why.
-
Test across lock states: Test your app’s file access in different device lock states to ensure proper behavior.
-
Consider .completeUnlessOpen carefully: Only use this for files that legitimately need to stay open across device locks (like active database connections).
Attack Scenarios
Understanding how unprotected files can be exploited:
Scenario 1: Physical Device Access
1. Attacker steals locked iPhone with .noFileProtection files
2. Using forensic tools, attacker extracts file system image
3. All files with .noFileProtection are readable without unlocking device
4. Attacker gains access to credentials, tokens, personal data
Scenario 2: Malware Extraction
1. Device is infected with malware (through compromised app or jailbreak)
2. Malware reads all files with .noFileProtection
3. Sensitive data is exfiltrated even while device appears locked
4. User is unaware of data breach
Scenario 3: Backup Compromise
1. User’s iCloud or iTunes backup is compromised
2. Backups contain files with .noFileProtection in cleartext
3. Attacker extracts sensitive data from backup without device passcode
4. All historical data in unprotected files is exposed
Scenario 4: Lost/Stolen Device
1. User loses device at airport, device is locked
2. Finder uses DFU mode and forensic software to extract files
3. Files with .noFileProtection are immediately accessible
4. Contains user’s banking credentials, stored passwords
Data Protection Class Selection Guide
Choose the appropriate protection level based on your data sensitivity and access requirements:
Use .complete when:
- Storing user credentials (passwords, API keys, tokens)
- Storing personal information (SSN, passport data, addresses)
- Storing financial data (credit cards, account numbers, transaction history)
- Storing health records or medical information
- Storing private keys or cryptographic material
- Storing any data that should never be accessible when device is locked
Use .completeUnlessOpen when:
- Managing active database files that must remain accessible during background updates
- Handling media files being actively streamed or processed
- Managing temporary files for long-running operations
- Never use for credentials or highly sensitive data
Use .completeUntilFirstUserAuthentication when:
- Storing app configuration that background processes need
- Storing data for background sync operations
- Storing notification payloads for processing
- Storing cached data that improves launch performance
- Never use for user credentials or personal information
Never use .noFileProtection for:
- Any sensitive data whatsoever
- User-generated content
- Authentication tokens or session data
- Any data you wouldn’t want publicly readable
Configuration
This detector can be configured with the list of weak file protection attributes to detect:
weakFileProtection:
- FileProtectionType.none
- NSFileProtectionNone
- URLFileProtection.none
You can add additional protection levels to this list if your security policy requires stricter controls.
References
-
CWE-311: Missing Encryption of Sensitive Data.
-
CWE-312: Cleartext Storage of Sensitive Information.
-
CWE-359: Exposure of Private Personal Information to an Unauthorized Actor.
-
OWASP Top 10 2021 A02 - Cryptographic Failures.
-
Apple Developer: FileProtectionType - Protection level values that can be associated with the
protectionKeykey. -
MASWE-0006: Sensitive Data Stored Unencrypted in Private Storage Locations.
-
MASWE-0007: Sensitive Data Stored Unencrypted in Shared Storage Requiring No User Interaction.