# Decidim's private data exports can lead to data leaks

Decidim's PrivateExport UUIDs stored as bigint in active_storage_attachments collide, letting one user download another user's private data export ZIP.

- Payout: Research
- Severity: high
- Program: Decidim
- Found by: ahukkanen
- Disclosed: 2026-02-03
- CVE: CVE-2025-65017
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-3cx6-j9j4-54mp
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-3cx6-j9j4-54mp
- Tags: area:web domain:web impact:data-leak impact:pii-leak kind:research platform:ghsa sev:high tech:ruby technique:race-window technique:source-review vuln:access-control vuln:info-disclosure

**Target:** Rails ActiveStorage attachment record_id on Decidim::PrivateExport
**Root cause:** PrivateExport uses an ActiveStorage file relation whose active_storage_attachments.record_id is a bigint, so UUID primary keys are coerced via to_i, truncating to a numeric prefix and colliding across exports.
**Steps:**
1. Run `for i in {1..10}; do bundle exec rspec spec/jobs/decidim/download_your_data_export_job_spec.rb -e "deletes the" || break ; done` repeatedly until a UUID converts to 0 via `.to_i`.
2. Create two users' private_exports with predefined colliding UUIDs like `0210ae70-482b-4671-b758-35e13e0097a9` and `0210d2df-a0c7-40aa-ad97-2dae5083e3b8`, attaching distinct ZIP buffers.
3. Log in as user 1, visit `/download_your_data`, click "Download file" and observe user 2's ZIP contents.
4. Confirm collision prefixes: UUIDs starting with `00000210`, `0000210`, `000210`, `00210`, `0210` or `210` all map to the same record_id.
**How found:** A flaky spec (`download_your_data_export_job_spec.rb`) failed intermittently; investigating the random failure revealed UUIDs converting to 0 via `.to_i`, then tracing the ActiveStorage attachment schema showed record_id is bigint, producing collisions.
**Exploitation:** Any user requesting a private data export whose UUID numeric prefix matches another user's export record_id causes the ActiveStorage attachment to resolve to the wrong export, so the victim downloads the other user's ZIP containing their personal data.
**Insight:** When a model uses UUID primary keys but attaches ActiveStorage files, check the attachments table's record_id column type — bigint coercion silently truncates UUIDs to numeric prefixes, and any UUID beginning with digits collides with shorter numeric IDs.

## Proof of concept (copied exactly from the original report)

**Ruby script creating colliding private exports**

```
# Create the ZIP buffers to be stored
buffer1 = Zip::OutputStream.write_buffer do |out|
  out.put_next_entry("admin.txt")
  out.write "Hello, admin!"
end
buffer1.rewind
buffer2 = Zip::OutputStream.write_buffer do |out|
  out.put_next_entry("user.txt")
  out.write "Hello, user!"
end
buffer2.rewind

# Create the private exports with a predefined IDs
user1 = Decidim::User.find(1)
export = user1.private_exports.build
export.id = "0210ae70-482b-4671-b758-35e13e0097a9"
export.export_type = "download_your_data"
export.file.attach(io: buffer1, filename: "foobar.zip", content_type: "application/zip")
export.expires_at = Decidim.download_your_data_expiry_time.from_now
export.metadata = {}
export.save!
```

**Spec loop to trigger UUID collision**

```
$ cd decidim-core
$ for i in {1..10}; do bundle exec rspec spec/jobs/decidim/download_your_data_export_job_spec.rb -e "deletes the" || break ; done
```

**Check file attached for zero UUID**

```
user.private_exports.last.file.attached?
=> false
user.private_exports.last.file.blob
=> nil
```

