|
| 1 | +# CSV Injection Security Fix |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This document describes the security vulnerability fix for CSV formula injection attacks in the SYNCRO application. The fix addresses potential security issues in both CSV import and export functionality. |
| 6 | + |
| 7 | +## Vulnerability Description |
| 8 | + |
| 9 | +### What is CSV Injection? |
| 10 | + |
| 11 | +CSV injection (also known as Formula Injection) is a vulnerability where malicious content in CSV files can be executed when opened in spreadsheet applications like Microsoft Excel, Google Sheets, or LibreOffice Calc. |
| 12 | + |
| 13 | +### Attack Vectors |
| 14 | + |
| 15 | +Cells beginning with the following characters can be interpreted as formulas: |
| 16 | +- `=` (equals) - Standard formula prefix |
| 17 | +- `+` (plus) - Alternative formula prefix |
| 18 | +- `-` (minus) - Alternative formula prefix |
| 19 | +- `@` (at) - Alternative formula prefix |
| 20 | +- `\t` (tab) - Can trigger formula execution |
| 21 | +- `\r` (carriage return) - Can trigger formula execution |
| 22 | + |
| 23 | +### Example Attacks |
| 24 | + |
| 25 | +```csv |
| 26 | +name,email,notes |
| 27 | +=cmd|"/c calc",user@example.com,Safe note |
| 28 | +=HYPERLINK("http://evil.com","Click here"),admin@example.com,Phishing |
| 29 | +@SUM(A1:A10)*cmd|'/c calc'!A0,test@example.com,DDE attack |
| 30 | +``` |
| 31 | + |
| 32 | +When opened in Excel/Sheets, these can: |
| 33 | +- Execute arbitrary system commands |
| 34 | +- Exfiltrate data to external servers |
| 35 | +- Launch malicious applications |
| 36 | +- Access sensitive files |
| 37 | + |
| 38 | +## Fix Implementation |
| 39 | + |
| 40 | +### 1. CSV Import Protection (`csv-import-service.ts`) |
| 41 | + |
| 42 | +**Location**: `/backend/src/services/csv-import-service.ts` |
| 43 | + |
| 44 | +**Implementation**: |
| 45 | +```typescript |
| 46 | +function detectFormulaInjection(value: string): boolean { |
| 47 | + if (!value) return false; |
| 48 | + const dangerousChars = ['=', '+', '-', '@', '\t', '\r']; |
| 49 | + return dangerousChars.some((char) => value.startsWith(char)); |
| 50 | +} |
| 51 | + |
| 52 | +function validateCellSafety(raw: Record<string, string>): string | null { |
| 53 | + for (const [key, value] of Object.entries(raw)) { |
| 54 | + if (detectFormulaInjection(String(value ?? ''))) { |
| 55 | + return `Cell "${key}" contains potentially dangerous formula character at start: "${value.substring(0, 10)}..."`; |
| 56 | + } |
| 57 | + } |
| 58 | + return null; |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +**Behavior**: |
| 63 | +- Rejects CSV rows containing cells that start with dangerous characters |
| 64 | +- Returns clear error messages identifying the problematic cell |
| 65 | +- Prevents malicious data from being stored in the database |
| 66 | + |
| 67 | +### 2. CSV Export Protection |
| 68 | + |
| 69 | +#### Backend Exports |
| 70 | + |
| 71 | +**Renewal History Service** (`renewal-history.service.ts`): |
| 72 | +```typescript |
| 73 | +private sanitizeCSVCell(value: any): string { |
| 74 | + if (value === null || value === undefined) return ''; |
| 75 | + |
| 76 | + const stringValue = String(value); |
| 77 | + const dangerousChars = ['=', '+', '-', '@', '\t', '\r']; |
| 78 | + |
| 79 | + // Prevent formula injection |
| 80 | + if (dangerousChars.some((char) => stringValue.startsWith(char))) { |
| 81 | + return `'${stringValue}`; |
| 82 | + } |
| 83 | + |
| 84 | + // Escape quotes and wrap in quotes if contains comma, newline, or quote |
| 85 | + if (stringValue.includes(',') || stringValue.includes('\n') || stringValue.includes('"')) { |
| 86 | + return `"${stringValue.replace(/"/g, '""')}"`; |
| 87 | + } |
| 88 | + |
| 89 | + return stringValue; |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +**Privacy Metrics** (`privacy-metrics.ts`): |
| 94 | +- Added `sanitizeCSVCell()` function with the same protection logic |
| 95 | +- Applied to all headers and data cells |
| 96 | + |
| 97 | +#### Client-Side Exports |
| 98 | + |
| 99 | +**Location**: `/client/lib/csv-utils.ts` |
| 100 | + |
| 101 | +**Implementation**: |
| 102 | +```typescript |
| 103 | +export const sanitizeCSVCell = (value: any): string => { |
| 104 | + if (value === null || value === undefined) return "" |
| 105 | + |
| 106 | + const stringValue = String(value) |
| 107 | + |
| 108 | + // Prevent CSV injection by escaping cells that start with special characters |
| 109 | + const dangerousChars = ["=", "+", "-", "@", "\t", "\r"] |
| 110 | + if (dangerousChars.some((char) => stringValue.startsWith(char))) { |
| 111 | + return `'${stringValue}` |
| 112 | + } |
| 113 | + |
| 114 | + // Escape quotes and wrap in quotes if contains comma, newline, or quote |
| 115 | + if (stringValue.includes(",") || stringValue.includes("\n") || stringValue.includes('"')) { |
| 116 | + return `"${stringValue.replace(/"/g, '""')}"` |
| 117 | + } |
| 118 | + |
| 119 | + return stringValue |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +**Usage**: Applied to all client-side CSV exports via `generateSafeCSV()` function |
| 124 | + |
| 125 | +### 3. Sanitization Strategy |
| 126 | + |
| 127 | +**Prefix with Single Quote**: When a dangerous character is detected at the start of a cell: |
| 128 | +``` |
| 129 | +=1+1 → '=1+1 |
| 130 | ++1234567890 → '+1234567890 |
| 131 | +-rm -rf / → '-rm -rf / |
| 132 | +@IMPORT → '@IMPORT |
| 133 | +``` |
| 134 | + |
| 135 | +**Why This Works**: |
| 136 | +- Single quote prefix forces spreadsheet applications to treat the content as text |
| 137 | +- Preserves the original data (visible to users) |
| 138 | +- Prevents formula execution |
| 139 | + |
| 140 | +**Important**: Characters in the middle of content are NOT escaped: |
| 141 | +``` |
| 142 | +Netflix (+HD) → Netflix (+HD) (unchanged) |
| 143 | +user@example.com → user@example.com (unchanged) |
| 144 | +Discount -50% → Discount -50% (unchanged) |
| 145 | +``` |
| 146 | + |
| 147 | +## Test Coverage |
| 148 | + |
| 149 | +### Backend Tests |
| 150 | + |
| 151 | +**CSV Import Tests** (`csv-import-service.test.ts`): |
| 152 | +- ✅ Rejects cells starting with `=` |
| 153 | +- ✅ Rejects cells starting with `+` |
| 154 | +- ✅ Rejects cells starting with `-` |
| 155 | +- ✅ Rejects cells starting with `@` |
| 156 | +- ✅ Rejects cells starting with tab character |
| 157 | +- ✅ Rejects cells starting with carriage return |
| 158 | +- ✅ Accepts formula characters not at the start |
| 159 | +- ✅ Rejects complex formula injection attempts |
| 160 | +- ✅ Proper error messages for malformed rows |
| 161 | + |
| 162 | +**Renewal History Export Tests** (`renewal-history-csv-export.test.ts`): |
| 163 | +- ✅ Sanitizes cells starting with dangerous characters |
| 164 | +- ✅ Handles normal text content |
| 165 | +- ✅ Preserves CSV structure |
| 166 | +- ✅ Handles null/undefined values |
| 167 | +- ✅ Handles empty rows |
| 168 | + |
| 169 | +**Privacy Metrics Export Tests** (`privacy-metrics-csv-export.test.ts`): |
| 170 | +- ✅ Sanitizes all fields |
| 171 | +- ✅ Handles null values |
| 172 | +- ✅ Maintains proper CSV format |
| 173 | +- ✅ Correct content-type headers |
| 174 | + |
| 175 | +### Client-Side Tests |
| 176 | + |
| 177 | +**CSV Utils Tests** (`csv-utils.test.ts`): |
| 178 | +- ✅ Formula injection protection for all dangerous characters |
| 179 | +- ✅ CSV formatting (commas, quotes, newlines) |
| 180 | +- ✅ Combined attack scenarios |
| 181 | +- ✅ Real-world DDE attacks |
| 182 | +- ✅ Hyperlink injection prevention |
| 183 | +- ✅ Cell reference exploitation prevention |
| 184 | + |
| 185 | +## Acceptance Criteria |
| 186 | + |
| 187 | +✅ **Formula-injection sanitization on export** |
| 188 | +- All CSV exports apply `sanitizeCSVCell()` to every cell |
| 189 | +- Dangerous characters at cell start are prefixed with single quote |
| 190 | +- Backend: `renewal-history.service.ts`, `privacy-metrics.ts` |
| 191 | +- Client: `csv-utils.ts` with `generateSafeCSV()` |
| 192 | + |
| 193 | +✅ **Malformed-row rejection on import** |
| 194 | +- CSV import validates every cell for formula injection |
| 195 | +- Rows with dangerous content are rejected with clear error messages |
| 196 | +- Import preview shows which rows failed and why |
| 197 | + |
| 198 | +✅ **Comprehensive tests** |
| 199 | +- 50+ test cases covering import validation and export sanitization |
| 200 | +- Tests for all dangerous characters: `=`, `+`, `-`, `@`, `\t`, `\r` |
| 201 | +- Real-world attack scenarios (DDE, hyperlink injection, cell references) |
| 202 | +- Edge cases (null values, empty strings, whitespace) |
| 203 | + |
| 204 | +## Security Considerations |
| 205 | + |
| 206 | +### What This Fix Prevents |
| 207 | +- ✅ Command execution via DDE (Dynamic Data Exchange) |
| 208 | +- ✅ Hyperlink-based phishing attacks |
| 209 | +- ✅ Data exfiltration through external references |
| 210 | +- ✅ Malicious macro triggers |
| 211 | + |
| 212 | +### What This Fix Does NOT Prevent |
| 213 | +- ❌ Vulnerabilities in the spreadsheet application itself |
| 214 | +- ❌ User intentionally executing untrusted macros |
| 215 | +- ❌ Social engineering attacks outside of CSV context |
| 216 | + |
| 217 | +### Best Practices |
| 218 | +1. **Always validate on import**: Reject malicious data before it enters the system |
| 219 | +2. **Always sanitize on export**: Protect users from data that may have bypassed validation |
| 220 | +3. **Defense in depth**: Apply protection at both backend and frontend |
| 221 | +4. **Clear error messages**: Help users understand why their CSV was rejected |
| 222 | + |
| 223 | +## References |
| 224 | + |
| 225 | +- [OWASP: CSV Injection](https://owasp.org/www-community/attacks/CSV_Injection) |
| 226 | +- [CSV Injection Revisited](https://www.contextis.com/en/blog/comma-separated-vulnerabilities) |
| 227 | +- [PayloadsAllTheThings: CSV Injection](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/CSV%20Injection) |
| 228 | + |
| 229 | +## Files Modified |
| 230 | + |
| 231 | +### Backend |
| 232 | +- ✅ `/backend/src/services/csv-import-service.ts` - Added validation |
| 233 | +- ✅ `/backend/src/subscription-renewal-history-timeline/renewal-history.service.ts` - Added sanitization |
| 234 | +- ✅ `/backend/src/routes/admin/privacy-metrics.ts` - Added sanitization |
| 235 | +- ✅ `/backend/tests/csv-import-service.test.ts` - Already had comprehensive tests |
| 236 | +- ✅ `/backend/tests/renewal-history-csv-export.test.ts` - Already had comprehensive tests |
| 237 | +- ✅ `/backend/tests/privacy-metrics-csv-export.test.ts` - Added new tests |
| 238 | + |
| 239 | +### Client |
| 240 | +- ✅ `/client/lib/csv-utils.ts` - Already had sanitization |
| 241 | +- ✅ `/client/lib/__tests__/csv-utils.test.ts` - Already had comprehensive tests |
| 242 | +- ✅ `/client/lib/csv-export.ts` - Uses `generateSafeCSV()` |
| 243 | +- ✅ `/client/hooks/use-bulk-actions.ts` - Uses `generateSafeCSV()` |
| 244 | + |
| 245 | +## Verification Steps |
| 246 | + |
| 247 | +1. **Import Protection**: |
| 248 | + ```bash |
| 249 | + cd backend |
| 250 | + npm test -- csv-import-service.test.ts |
| 251 | + ``` |
| 252 | + |
| 253 | +2. **Export Protection**: |
| 254 | + ```bash |
| 255 | + cd backend |
| 256 | + npm test -- renewal-history-csv-export.test.ts |
| 257 | + npm test -- privacy-metrics-csv-export.test.ts |
| 258 | + ``` |
| 259 | + |
| 260 | +3. **Client Protection**: |
| 261 | + ```bash |
| 262 | + cd client |
| 263 | + npm test -- csv-utils.test.ts |
| 264 | + ``` |
| 265 | + |
| 266 | +4. **Manual Testing**: |
| 267 | + - Try importing a CSV with `=1+1` in the name field → Should reject |
| 268 | + - Export subscriptions and verify dangerous characters are prefixed with `'` |
| 269 | + - Open exported CSV in Excel → Should display as text, not execute |
| 270 | + |
| 271 | +## Conclusion |
| 272 | + |
| 273 | +This security fix provides comprehensive protection against CSV injection attacks across all import and export functionality in the SYNCRO application. The implementation follows industry best practices and is backed by extensive test coverage. |
0 commit comments