@@ -343,22 +343,34 @@ final class ExportService: ObservableObject {
343343 }
344344
345345 private func escapeCSVField( _ field: String , options: CSVExportOptions ) -> String {
346+ var processed = field
347+
348+ // Sanitize formula-like prefixes to prevent CSV formula injection
349+ // Values starting with these characters can be executed as formulas in Excel/LibreOffice
350+ if options. sanitizeFormulas {
351+ let dangerousPrefixes : [ Character ] = [ " = " , " + " , " - " , " @ " , " \t " , " \r " ]
352+ if let first = processed. first, dangerousPrefixes. contains ( first) {
353+ // Prefix with single quote - Excel/LibreOffice treats this as text
354+ processed = " ' " + processed
355+ }
356+ }
357+
346358 switch options. quoteHandling {
347359 case . always:
348- let escaped = field . replacingOccurrences ( of: " \" " , with: " \" \" " )
360+ let escaped = processed . replacingOccurrences ( of: " \" " , with: " \" \" " )
349361 return " \" \( escaped) \" "
350362 case . never:
351- return field
363+ return processed
352364 case . asNeeded:
353- let needsQuotes = field . contains ( options. delimiter. actualValue) ||
354- field . contains ( " \" " ) ||
355- field . contains ( " \n " ) ||
356- field . contains ( " \r " )
365+ let needsQuotes = processed . contains ( options. delimiter. actualValue) ||
366+ processed . contains ( " \" " ) ||
367+ processed . contains ( " \n " ) ||
368+ processed . contains ( " \r " )
357369 if needsQuotes {
358- let escaped = field . replacingOccurrences ( of: " \" " , with: " \" \" " )
370+ let escaped = processed . replacingOccurrences ( of: " \" " , with: " \" \" " )
359371 return " \" \( escaped) \" "
360372 }
361- return field
373+ return processed
362374 }
363375 }
364376
@@ -416,7 +428,7 @@ final class ExportService: ObservableObject {
416428 isFirstField = false
417429
418430 let escapedKey = escapeJSONString ( column)
419- let jsonValue = formatJSONValue ( value)
431+ let jsonValue = formatJSONValue ( value, preserveAsString : config . jsonOptions . preserveAllAsStrings )
420432 try fileHandle. write ( contentsOf: " \" \( escapedKey) \" : \( jsonValue) " . toUTF8Data ( ) )
421433 }
422434 }
@@ -462,9 +474,18 @@ final class ExportService: ObservableObject {
462474 }
463475
464476 /// Format a value for JSON output
465- private func formatJSONValue( _ value: String ? ) -> String {
477+ /// - Parameters:
478+ /// - value: The value to format
479+ /// - preserveAsString: If true, always output as string without type detection
480+ /// (preserves leading zeros in ZIP codes, phone numbers, etc.)
481+ private func formatJSONValue( _ value: String ? , preserveAsString: Bool ) -> String {
466482 guard let val = value else { return " null " }
467483
484+ // If preserving all as strings, skip type detection
485+ if preserveAsString {
486+ return " \" \( escapeJSONString ( val) ) \" "
487+ }
488+
468489 // Try to detect numbers and booleans
469490 if let intVal = Int ( val) {
470491 return String ( intVal)
@@ -614,8 +635,8 @@ final class ExportService: ObservableObject {
614635
615636 let values = row. map { value -> String in
616637 guard let val = value else { return " NULL " }
617- // Escape single quotes by doubling them
618- let escaped = val . replacingOccurrences ( of : " ' " , with : " '' " )
638+ // Use proper SQL escaping to prevent injection (handles backslashes, quotes, etc.)
639+ let escaped = SQLEscaping . escapeStringLiteral ( val )
619640 return " ' \( escaped) ' "
620641 } . joined ( separator: " , " )
621642
@@ -646,8 +667,6 @@ final class ExportService: ObservableObject {
646667 process. arguments = [ " -c " , source. path]
647668
648669 let outputFile = try FileHandle ( forWritingTo: destination)
649- defer {
650- try ? outputFile. close ( )
651670 defer {
652671 try ? outputFile. close ( )
653672 }
0 commit comments