using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
public static class CsvInjectionSecurityHelper
{
// Dangerous formula prefixes that can execute code in spreadsheet applications
private static readonly char[] DangerousFormulaPrefixes = { '=', '+', '-', '@', '\t', '\r' };
// Additional dangerous characters that could be used in formulas
private static readonly string[] DangerousPatterns = {
"=cmd|", // Command execution
"=system(", // System command
"=exec(", // Execute command
"=shell(", // Shell command
"=powershell", // PowerShell execution
"=bash", // Bash command
"=sh", // Shell command
"=python", // Python execution
"=perl", // Perl execution
"=ruby", // Ruby execution
"=javascript:", // JavaScript execution
"=hyperlink(", // Hyperlink injection
"=importxml(", // XML import (Google Sheets)
"=importhtml(", // HTML import (Google Sheets)
"=importdata(", // Data import (Google Sheets)
"=importfeed(", // Feed import (Google Sheets)
"=importrange(", // Range import (Google Sheets)
"=googletranslate(", // Google Translate function
"=webservice(", // Web service call
"=filterxml(", // XML filtering
"=dde|", // Dynamic Data Exchange
"=document.", // Document object access
"=window.", // Window object access
"=eval(", // Eval function
"=setTimeout(", // Timeout function
"=setInterval(", // Interval function
};
/// <summary>
/// Validates input for CSV injection patterns
/// </summary>
/// <param name="input">The input string to validate</param>
/// <returns>True if input appears safe, false if potentially dangerous</returns>
public static bool IsValidForCsv(string input)
{
if (string.IsNullOrEmpty(input))
return true;
// Check for dangerous formula prefixes
if (input.Length > 0 && DangerousFormulaPrefixes.Contains(input[0]))
return false;
// Check for dangerous patterns (case-insensitive)
foreach (string pattern in DangerousPatterns)
{
if (input.IndexOf(pattern, StringComparison.OrdinalIgnoreCase) >= 0)
return false;
}
// Check for suspicious patterns that might indicate formula injection
if (ContainsSuspiciousFormulas(input))
return false;
return true;
}
/// <summary>
/// Sanitizes input to prevent CSV injection
/// </summary>
/// <param name="input">The input string to sanitize</param>
/// <param name="strategy">Sanitization strategy to use</param>
/// <returns>Sanitized string safe for CSV output</returns>
public static string SanitizeForCsv(string input, CsvSanitizationStrategy strategy = CsvSanitizationStrategy.PrefixWithApostrophe)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
switch (strategy)
{
case CsvSanitizationStrategy.PrefixWithApostrophe:
return SanitizeWithApostrophe(input);
case CsvSanitizationStrategy.PrefixWithSpace:
return SanitizeWithSpace(input);
case CsvSanitizationStrategy.RemoveDangerousCharacters:
return RemoveDangerousCharacters(input);
case CsvSanitizationStrategy.EncodeSpecialCharacters:
return EncodeSpecialCharacters(input);
case CsvSanitizationStrategy.RejectDangerous:
return IsValidForCsv(input) ? input : "[REJECTED_CONTENT]";
default:
return SanitizeWithApostrophe(input);
}
}
/// <summary>
/// Sanitization strategies for CSV content
/// </summary>
public enum CsvSanitizationStrategy
{
/// <summary>Prefix dangerous content with apostrophe to treat as text</summary>
PrefixWithApostrophe,
/// <summary>Prefix dangerous content with space</summary>
PrefixWithSpace,
/// <summary>Remove dangerous characters entirely</summary>
RemoveDangerousCharacters,
/// <summary>Encode special characters</summary>
EncodeSpecialCharacters,
/// <summary>Reject dangerous content with placeholder</summary>
RejectDangerous
}
/// <summary>
/// Sanitizes by prefixing with apostrophe (most common approach)
/// </summary>
/// <param name="input">Input to sanitize</param>
/// <returns>Sanitized string</returns>
private static string SanitizeWithApostrophe(string input)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
// If input starts with dangerous character, prefix with apostrophe
if (input.Length > 0 && DangerousFormulaPrefixes.Contains(input[0]))
return "'" + input;
// Check for dangerous patterns and prefix if found
foreach (string pattern in DangerousPatterns)
{
if (input.IndexOf(pattern, StringComparison.OrdinalIgnoreCase) >= 0)
return "'" + input;
}
return input;
}
/// <summary>
/// Sanitizes by prefixing with space
/// </summary>
/// <param name="input">Input to sanitize</param>
/// <returns>Sanitized string</returns>
private static string SanitizeWithSpace(string input)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
if (input.Length > 0 && DangerousFormulaPrefixes.Contains(input[0]))
return " " + input;
return input;
}
/// <summary>
/// Removes dangerous characters entirely
/// </summary>
/// <param name="input">Input to sanitize</param>
/// <returns>Sanitized string</returns>
private static string RemoveDangerousCharacters(string input)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
var result = new StringBuilder();
foreach (char c in input)
{
if (!DangerousFormulaPrefixes.Contains(c))
result.Append(c);
}
return result.ToString();
}
/// <summary>
/// Encodes special characters
/// </summary>
/// <param name="input">Input to sanitize</param>
/// <returns>Sanitized string</returns>
private static string EncodeSpecialCharacters(string input)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
return input.Replace("=", "=")
.Replace("+", "+")
.Replace("-", "-")
.Replace("@", "@")
.Replace("\t", "	")
.Replace("\r", " ");
}
/// <summary>
/// Checks for suspicious formula patterns
/// </summary>
/// <param name="input">Input to check</param>
/// <returns>True if suspicious patterns found</returns>
private static bool ContainsSuspiciousFormulas(string input)
{
// Check for patterns that might indicate formula injection
var suspiciousPatterns = new[]
{
@"=\s*\w+\s*\(", // Function calls
@"=.*\|.*\|", // Pipe characters (DDE)
@"=.*cmd.*", // Command references
@"=.*powershell.*", // PowerShell references
@"=.*javascript:.*", // JavaScript protocol
@"=.*file:\/\/.*", // File protocol
@"=.*http[s]?:\/\/.*", // HTTP protocols in formulas
};
foreach (string pattern in suspiciousPatterns)
{
if (Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase))
return true;
}
return false;
}
/// <summary>
/// Validates an entire CSV row for injection attacks
/// </summary>
/// <param name="csvRow">Array of CSV field values</param>
/// <returns>True if all fields are safe, false otherwise</returns>
public static bool IsValidCsvRow(string[] csvRow)
{
if (csvRow == null)
return true;
return csvRow.All(field => IsValidForCsv(field));
}
/// <summary>
/// Sanitizes an entire CSV row
/// </summary>
/// <param name="csvRow">Array of CSV field values</param>
/// <param name="strategy">Sanitization strategy</param>
/// <returns>Sanitized CSV row</returns>
public static string[] SanitizeCsvRow(string[] csvRow, CsvSanitizationStrategy strategy = CsvSanitizationStrategy.PrefixWithApostrophe)
{
if (csvRow == null)
return null;
return csvRow.Select(field => SanitizeForCsv(field, strategy)).ToArray();
}
/// <summary>
/// Validates CSV content for bulk operations
/// </summary>
/// <param name="csvData">List of CSV rows</param>
/// <param name="dangerousRows">Output list of row indices that contain dangerous content</param>
/// <returns>True if all rows are safe, false otherwise</returns>
public static bool ValidateCsvData(List<string[]> csvData, out List<int> dangerousRows)
{
dangerousRows = new List<int>();
if (csvData == null || csvData.Count == 0)
return true;
for (int i = 0; i < csvData.Count; i++)
{
if (!IsValidCsvRow(csvData[i]))
{
dangerousRows.Add(i);
}
}
return dangerousRows.Count == 0;
}
/// <summary>
/// Sanitizes CSV data for bulk operations
/// </summary>
/// <param name="csvData">List of CSV rows</param>
/// <param name="strategy">Sanitization strategy</param>
/// <returns>Sanitized CSV data</returns>
public static List<string[]> SanitizeCsvData(List<string[]> csvData, CsvSanitizationStrategy strategy = CsvSanitizationStrategy.PrefixWithApostrophe)
{
if (csvData == null)
return null;
return csvData.Select(row => SanitizeCsvRow(row, strategy)).ToList();
}
}
// CSV Export Helper with built-in security
public static class SecureCsvExporter
{
/// <summary>
/// Exports data to CSV with automatic sanitization
/// </summary>
/// <param name="data">Data to export</param>
/// <param name="headers">CSV headers</param>
/// <param name="strategy">Sanitization strategy</param>
/// <returns>Safe CSV content</returns>
public static string ExportToCsv(List<object[]> data, string[] headers,
CsvInjectionSecurityHelper.CsvSanitizationStrategy strategy = CsvInjectionSecurityHelper.CsvSanitizationStrategy.PrefixWithApostrophe)
{
var csvBuilder = new StringBuilder();
// Sanitize and add headers
if (headers != null)
{
var safeHeaders = headers.Select(h => CsvInjectionSecurityHelper.SanitizeForCsv(h?.ToString() ?? "", strategy)).ToArray();
csvBuilder.AppendLine(string.Join(",", safeHeaders.Select(EscapeCsvField)));
}
// Sanitize and add data rows
foreach (var row in data)
{
var stringRow = row.Select(field => field?.ToString() ?? "").ToArray();
var safeRow = CsvInjectionSecurityHelper.SanitizeCsvRow(stringRow, strategy);
csvBuilder.AppendLine(string.Join(",", safeRow.Select(EscapeCsvField)));
}
return csvBuilder.ToString();
}
/// <summary>
/// Properly escapes CSV field values
/// </summary>
/// <param name="field">Field value to escape</param>
/// <returns>Escaped field value</returns>
private static string EscapeCsvField(string field)
{
if (string.IsNullOrEmpty(field))
return string.Empty;
// If field contains comma, quote, or newline, wrap in quotes and escape internal quotes
if (field.Contains(",") || field.Contains("\"") || field.Contains("\n") || field.Contains("\r"))
{
return "\"" + field.Replace("\"", "\"\"") + "\"";
}
return field;
}
}
// Example usage
public class CsvSecurityExample
{
public void DemonstrateUsage()
{
// Example of dangerous CSV content
var dangerousInputs = new[]
{
"=cmd|' /C calc'!A0", // Command execution
"=SUM(1+1)*cmd|' /C calc'!A0", // Hidden command in formula
"=2+5+cmd|' /C calc'!A0", // Command in calculation
"+2+5+cmd|' /C calc'!A0", // Plus prefix
"-2+5+cmd|' /C calc'!A0", // Minus prefix
"@SUM(1+1)*cmd|' /C calc'!A0", // @ prefix
"=HYPERLINK(\"http://evil.com\",\"click me\")", // Hyperlink injection
"=IMPORTXML(\"http://evil.com/\",\"//\")" // XML import
};
Console.WriteLine("=== CSV Injection Validation Examples ===");
foreach (var input in dangerousInputs)
{
bool isValid = CsvInjectionSecurityHelper.IsValidForCsv(input);
string sanitized = CsvInjectionSecurityHelper.SanitizeForCsv(input);
Console.WriteLine($"Input: {input}");
Console.WriteLine($"Valid: {isValid}");
Console.WriteLine($"Sanitized: {sanitized}");
Console.WriteLine();
}
// Example of CSV export with security
var userData = new List<object[]>
{
new object[] { "John Doe", "john@example.com", "=SUM(1+1)" },
new object[] { "Jane Smith", "jane@example.com", "+1234567890" },
new object[] { "Bob Johnson", "bob@example.com", "Normal data" }
};
var headers = new[] { "Name", "Email", "Notes" };
string safeCsv = SecureCsvExporter.ExportToCsv(userData, headers);
Console.WriteLine("=== Secure CSV Export ===");
Console.WriteLine(safeCsv);
}
/// <summary>
/// Example of validating uploaded CSV file
/// </summary>
/// <param name="csvContent">Raw CSV content</param>
public void ValidateUploadedCsv(string csvContent)
{
// Parse CSV (using a CSV parsing library like CsvHelper in real scenarios)
var rows = ParseCsvContent(csvContent);
// Validate for CSV injection
if (CsvInjectionSecurityHelper.ValidateCsvData(rows, out List<int> dangerousRows))
{
Console.WriteLine("CSV file is safe to process.");
}
else
{
Console.WriteLine($"CSV file contains dangerous content in rows: {string.Join(", ", dangerousRows)}");
// Option 1: Reject the file
throw new InvalidOperationException("CSV file contains potentially dangerous content.");
// Option 2: Sanitize and continue
var sanitizedData = CsvInjectionSecurityHelper.SanitizeCsvData(rows);
Console.WriteLine("CSV data has been sanitized.");
}
}
/// <summary>
/// Simple CSV parsing for demonstration (use proper CSV library in production)
/// </summary>
/// <param name="csvContent">CSV content to parse</param>
/// <returns>Parsed CSV rows</returns>
private List<string[]> ParseCsvContent(string csvContent)
{
var rows = new List<string[]>();
var lines = csvContent.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
// Simple parsing - use proper CSV library for production
var fields = line.Split(',');
rows.Add(fields);
}
return rows;
}
}