From 81d52f47f6672a9eaff61a7023f68f1215eadad8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:48:07 +0000 Subject: [PATCH] fix(security): sanitize filenames to prevent path traversal - Refactor `DownloadImageProcessor.processName` to strictly sanitize filenames. - Refactor `AssetsProcessor.processNames` and `normalizeAssetName` for consistency. - Remove control characters, normalize separators, and replace `..` with `__`. --- .../Subcommands/DownloadImageProcessor.swift | 68 ++++++++++++++----- .../ExFigCore/Processor/AssetsProcessor.swift | 4 ++ 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/Sources/ExFig/Subcommands/DownloadImageProcessor.swift b/Sources/ExFig/Subcommands/DownloadImageProcessor.swift index 219fea58..8b32cdbe 100644 --- a/Sources/ExFig/Subcommands/DownloadImageProcessor.swift +++ b/Sources/ExFig/Subcommands/DownloadImageProcessor.swift @@ -48,29 +48,63 @@ enum DownloadImageProcessor { ) -> String { var result = name - // Apply regex replacement if both patterns are specified - if let validateRegexp, let replaceRegexp { - // Normalize path separators first - result = result.replacingOccurrences(of: "/", with: "_") - // Apply regex replacement - if let regex = try? NSRegularExpression(pattern: validateRegexp) { - let range = NSRange(result.startIndex..., in: result) - result = regex.stringByReplacingMatches( - in: result, - range: range, - withTemplate: replaceRegexp - ) - } - } else { - // Just normalize path separators - result = result.replacingOccurrences(of: "/", with: "_") + // 1. Initial sanitization: separators + result = result.replacingOccurrences(of: "/", with: "_") + result = result.replacingOccurrences(of: "\\", with: "_") + + // 2. Apply regex replacement if both patterns are specified + if let validateRegexp, let replaceRegexp, + let regex = try? NSRegularExpression(pattern: validateRegexp) + { + let range = NSRange(result.startIndex..., in: result) + result = regex.stringByReplacingMatches( + in: result, + range: range, + withTemplate: replaceRegexp + ) } - // Apply name style + // 3. Apply name style if let nameStyle { result = applyNameStyle(result, style: nameStyle) } + // 4. Final strict sanitization + // This ensures that even after regex/style application, the result is safe + result = sanitizeFilename(result) + + return result + } + + /// Strictly sanitizes a filename to prevent path traversal and ensure filesystem safety. + /// + /// - Parameter name: The name to sanitize + /// - Returns: Sanitized name safe for use as a filename + private static func sanitizeFilename(_ name: String) -> String { + var result = name + + // Replace any remaining path separators (just in case regex introduced them) + result = result.replacingOccurrences(of: "/", with: "_") + result = result.replacingOccurrences(of: "\\", with: "_") + + // Prevent path traversal + result = result.replacingOccurrences(of: "..", with: "__") + + // Remove control characters and other dangerous chars + // Reserved chars: : * ? " < > | (Windows) + control chars + let illegalChars = CharacterSet(charactersIn: ":*?\"<>|") + .union(.controlCharacters) + .union(.newlines) + .union(.illegalCharacters) + + let components = result.components(separatedBy: illegalChars) + result = components.joined(separator: "_") + + // Ensure non-empty result (fallback to "unnamed") + if result.isEmpty || result == "." || result == "_" { + return "unnamed" + } + return result } diff --git a/Sources/ExFigCore/Processor/AssetsProcessor.swift b/Sources/ExFigCore/Processor/AssetsProcessor.swift index a6220533..dfc05cc3 100644 --- a/Sources/ExFigCore/Processor/AssetsProcessor.swift +++ b/Sources/ExFigCore/Processor/AssetsProcessor.swift @@ -1,4 +1,6 @@ // swiftlint:disable file_length +import Foundation + /// Process asset name public protocol AssetNameProcessable { var nameReplaceRegexp: String? { get } @@ -114,6 +116,7 @@ public struct ImagesProcessor: AssetsProcessable, Sendable { result = String(split[0]) } else { result = result.replacingOccurrences(of: "/", with: "_") + result = result.replacingOccurrences(of: "\\", with: "_") } // Apply nameReplaceRegexp if configured @@ -460,6 +463,7 @@ public extension AssetsProcessable { renamedAsset.name = String(split[0]) } else { renamedAsset.name = renamedAsset.name.replacingOccurrences(of: "/", with: "_") + renamedAsset.name = renamedAsset.name.replacingOccurrences(of: "\\", with: "_") } return renamedAsset }