Basecamp · Report #3543475
Improper Access Control in `fizzy.do` import flow allows cross-tenant ActionText reference resolution and data disclosure
How it was found
Description
The account import flow processes ActionText attachment HTML from user-uploaded ZIP content.
In app/models/account/data_transfer/action_text_rich_text_record_set.rb, import-time method convert_gids_to_sgids converts attacker-controlled gid values into persisted sgid values by resolving the target record globally:
app/models/account/data_transfer/action_text_rich_text_record_set.rb:83app/models/account/data_transfer/action_text_rich_text_record_set.rb:87app/models/account/data_transfer/action_text_rich_text_record_set.rb:88app/models/account/data_transfer/action_text_rich_text_record_set.rb:89
The import path has no tenant ownership check before minting the signed reference.
Reachable import path from normal upload flow:
app/controllers/account/imports_controller.rb:38app/models/account/import.rb:37app/models/account/import.rb:39
For comparison, export path has an account check (record&.account_id == account.id) in the same file at app/models/account/data_transfer/action_text_rich_text_record_set.rb:69.
Reproduction Path A (Fastest for triage, deterministic)
This path is self-contained and exercises the real vulnerable import code path (import_batch) without requiring full web flow setup.
Prerequisites
- Ruby and Bundler installed
- Repo cloned
Steps
- Enter repo:
cd /path/to/fizzy
- Install/check dependencies:
bundle check || bundle install
- Run standalone integration PoC:
bundle exec ruby security-poc/integration_test_standalone.rb
- Expected success signals in output:
RESULT: IMPACT_CONFIRMED=trueimported.account_id = <attacker_account_id>resolved_account_id = <victim_account_id>(different tenant)attachable_text = @alice_secret
Why this is valid
This script loads and executes the real vulnerable file:
app/models/account/data_transfer/action_text_rich_text_record_set.rb
And runs:
Account::DataTransfer::ActionTextRichTextRecordSet#import_batch
So the exploit condition is validated directly against production logic.
Reproduction Path B (Full product-equivalent import flow)
This path uses the Rails environment and import conversion path end-to-end.
Steps
- Enter repo:
cd /path/to/fizzy
- Install/check dependencies:
bundle check || bundle install
- Run full import impact demo:
DISABLE_BOOTSNAP=1 RAILS_ENV=development \
bundle exec rails runner security-poc/demo_import_cross_account_impact.rb
- Expected success signals:
cross_account_reference=trueIMPACT_CONFIRMED=trueimported_rich_text_account_id=<attacker_account_id>resolved_account_id=<victim_account_id>leaked_attachable_text=@alice
Note
In slower environments, Rails boot may take a few minutes before script output appears.
Evidence from local execution
Observed in local runs:
- Path A (
integration_test_standalone.rb):RESULT: IMPACT_CONFIRMED=true - Path B (
demo_import_cross_account_impact.rb):IMPACT_CONFIRMED=true
Both runs showed attacker-owned imported rich text resolving a victim-owned attachable record.
Supporting files
security-poc/integration_test_standalone.rbsecurity-poc/demo_import_cross_account_impact.rbsecurity-poc/patch_action_text_gid.py
Suggested remediation
In convert_gids_to_sgids, only mint sgid when resolved record belongs to importing account:
- Resolve record safely (handle not found)
- Require
record.respond_to?(:account_id) && record.account_id == account.id - Drop or ignore cross-account references
Impact
Impact
- Cross-tenant unauthorized data reference in imported rich text.
- Victim-account attachable data is resolved/rendered in attacker-account context.
- Persisted unauthorized reference (
sgid) remains stored until removed.