harden(persistence): FileVault path traversal guard + error unification
按 code quality review 反馈(P0 + 4×P1): - 加 resolveSafePath() 拒绝 / 和 .. 并验证 hasPrefix(rootURL) - loadImage/remove 统一抛 FileVaultError(readFailed/removeFailed) - 删除测试 struct 上多余的 @MainActor - 每个 @Test 加 defer cleanup,不泄漏 temp 目录 - 测试图片改用生成 16x16 红色,不依赖 SF Symbol Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,23 @@ final class FileVault {
|
|||||||
let bytes: Int
|
let bytes: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Path Safety
|
||||||
|
|
||||||
|
private func resolveSafePath(_ relativePath: String) throws -> URL {
|
||||||
|
guard !relativePath.contains("/"),
|
||||||
|
!relativePath.contains(".."),
|
||||||
|
!relativePath.isEmpty else {
|
||||||
|
throw FileVaultError.readFailed
|
||||||
|
}
|
||||||
|
let url = rootURL.appendingPathComponent(relativePath)
|
||||||
|
guard url.path.hasPrefix(rootURL.path) else {
|
||||||
|
throw FileVaultError.readFailed
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public API
|
||||||
|
|
||||||
func writeJPEG(_ image: UIImage, quality: CGFloat = 0.85) throws -> SavedAsset {
|
func writeJPEG(_ image: UIImage, quality: CGFloat = 0.85) throws -> SavedAsset {
|
||||||
guard let data = image.jpegData(compressionQuality: quality) else {
|
guard let data = image.jpegData(compressionQuality: quality) else {
|
||||||
throw FileVaultError.writeFailed
|
throw FileVaultError.writeFailed
|
||||||
@@ -51,15 +68,24 @@ final class FileVault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadImage(relativePath: String) throws -> UIImage {
|
func loadImage(relativePath: String) throws -> UIImage {
|
||||||
let url = rootURL.appendingPathComponent(relativePath)
|
let url = try resolveSafePath(relativePath)
|
||||||
let data = try Data(contentsOf: url)
|
let data: Data
|
||||||
|
do {
|
||||||
|
data = try Data(contentsOf: url)
|
||||||
|
} catch {
|
||||||
|
throw FileVaultError.readFailed
|
||||||
|
}
|
||||||
guard let image = UIImage(data: data) else { throw FileVaultError.decodeFailed }
|
guard let image = UIImage(data: data) else { throw FileVaultError.decodeFailed }
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove(relativePath: String) throws {
|
func remove(relativePath: String) throws {
|
||||||
let url = rootURL.appendingPathComponent(relativePath)
|
let url = try resolveSafePath(relativePath)
|
||||||
try FileManager.default.removeItem(at: url)
|
do {
|
||||||
|
try FileManager.default.removeItem(at: url)
|
||||||
|
} catch {
|
||||||
|
throw FileVaultError.removeFailed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func wipe() throws {
|
func wipe() throws {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import Testing
|
|||||||
import UIKit
|
import UIKit
|
||||||
@testable import 体己
|
@testable import 体己
|
||||||
|
|
||||||
@MainActor
|
|
||||||
struct FileVaultTests {
|
struct FileVaultTests {
|
||||||
|
|
||||||
private func makeIsolatedVault() throws -> FileVault {
|
private func makeIsolatedVault() throws -> FileVault {
|
||||||
@@ -11,9 +10,19 @@ struct FileVaultTests {
|
|||||||
return try FileVault(rootURL: temp)
|
return try FileVault(rootURL: temp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func makeTestImage() -> UIImage {
|
||||||
|
let size = CGSize(width: 16, height: 16)
|
||||||
|
let renderer = UIGraphicsImageRenderer(size: size)
|
||||||
|
return renderer.image { ctx in
|
||||||
|
UIColor.red.setFill()
|
||||||
|
ctx.fill(CGRect(origin: .zero, size: size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test func writeAndReadJPEGRoundtrip() throws {
|
@Test func writeAndReadJPEGRoundtrip() throws {
|
||||||
let vault = try makeIsolatedVault()
|
let vault = try makeIsolatedVault()
|
||||||
let image = UIImage(systemName: "doc")!
|
defer { try? FileManager.default.removeItem(at: vault.rootURL) }
|
||||||
|
let image = makeTestImage()
|
||||||
|
|
||||||
let saved = try vault.writeJPEG(image, quality: 0.8)
|
let saved = try vault.writeJPEG(image, quality: 0.8)
|
||||||
|
|
||||||
@@ -26,7 +35,8 @@ struct FileVaultTests {
|
|||||||
|
|
||||||
@Test func removeMakesFileGone() throws {
|
@Test func removeMakesFileGone() throws {
|
||||||
let vault = try makeIsolatedVault()
|
let vault = try makeIsolatedVault()
|
||||||
let saved = try vault.writeJPEG(UIImage(systemName: "doc")!)
|
defer { try? FileManager.default.removeItem(at: vault.rootURL) }
|
||||||
|
let saved = try vault.writeJPEG(makeTestImage())
|
||||||
|
|
||||||
try vault.remove(relativePath: saved.relativePath)
|
try vault.remove(relativePath: saved.relativePath)
|
||||||
|
|
||||||
@@ -37,8 +47,9 @@ struct FileVaultTests {
|
|||||||
|
|
||||||
@Test func wipeRemovesAllFiles() throws {
|
@Test func wipeRemovesAllFiles() throws {
|
||||||
let vault = try makeIsolatedVault()
|
let vault = try makeIsolatedVault()
|
||||||
let a = try vault.writeJPEG(UIImage(systemName: "doc")!)
|
defer { try? FileManager.default.removeItem(at: vault.rootURL) }
|
||||||
let b = try vault.writeJPEG(UIImage(systemName: "folder")!)
|
let a = try vault.writeJPEG(makeTestImage())
|
||||||
|
let b = try vault.writeJPEG(makeTestImage())
|
||||||
|
|
||||||
try vault.wipe()
|
try vault.wipe()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user