Improper Access Control in `fizzy.do` import flow allows cross-tenant ActionText reference resolution and data disclosure
Low
Vulnerability Details
## 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:83`
- `app/models/account/data_transfer/action_text_rich_text_record_set.rb:87`
- `app/models/account/data_transfer/action_text_rich_text_record_set.rb:88`
- `app/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:38`
- `app/models/account/import.rb:37`
- `app/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
1. Enter repo:
```bash
cd /path/to/fizzy
```
2. Install/check dependencies:
```bash
bundle check || bundle install
```
3. Run standalone integration PoC:
```bash
bundle exec ruby security-poc/integration_test_standalone.rb
```
4. Expected success signals in output:
- `RESULT: IMPACT_CONFIRMED=true`
- `imported.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
1. Enter repo:
```bash
cd /path/to/fizzy
```
2. Install/check dependencies:
```bash
bundle check || bundle install
```
3. Run full import impact demo:
```bash
DISABLE_BOOTSNAP=1 RAILS_ENV=development \
bundle exec rails runner security-poc/demo_import_cross_account_impact.rb
```
4. Expected success signals:
- `cross_account_reference=true`
- `IMPACT_CONFIRMED=true`
- `imported_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.rb`
- `security-poc/demo_import_cross_account_impact.rb`
- `security-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.
Actions
View on HackerOneReport Stats
- Report ID: 3543475
- State: Closed
- Substate: resolved
- Upvotes: 4