Feb 3, 2026·5 min read·38 visits
Decidim used UUIDs for private exports, but the database column expected Integers. Ruby cast the UUIDs (e.g., 'a1b2...') to '0', causing different users' files to overwrite each other or be served interchangeably. Attackers could simply request their own data and receive someone else's sensitive ZIP file.
A critical type confusion vulnerability in Decidim's 'Download Your Data' feature turned a privacy compliance tool into a data leak hose. Due to a mismatch between Ruby on Rails' ActiveStorage schema and Decidim's use of UUIDs, unique export IDs were silently cast to integers, causing massive database collisions. This allowed users requesting their own data to accidentally download full data dumps belonging to other users.
Irony is the lifeblood of the security industry, and CVE-2025-65017 is a vintage bottle of it. Decidim, a platform designed for participatory democracy and citizen engagement, naturally has a 'Download Your Data' feature. It's a GDPR requirement, designed to give users control and transparency over their private information.
But in software engineering, the road to hell is paved with type mismatches. In versions 0.30.0 through 0.31.0, this exact feature—the one explicitly built to protect user agency—became a mechanism for unauthenticated (or low-privilege) mass data disclosure. The bug wasn't a complex buffer overflow or a sophisticated injection. It was a simple case of the database speaking one language (Integers) and the application speaking another (UUIDs), with the translator (Ruby) doing a terrible job in the middle.
Imagine walking into a coat check, handing them ticket 'A123', and the attendant ripping off the 'A', seeing '0', and handing you the coat belonging to the guy with ticket 'B456' (which also became '0'). That is effectively what happened here, but instead of coats, it was ZIP files containing voting history, private messages, and user metadata.
The root cause is a classic mismatch in the Active Record ecosystem. When Decidim introduced the PrivateExport model, they correctly decided to use UUIDs for primary keys. UUIDs are great; they are unguessable and prevent enumeration attacks. However, they attached files to this model using Rails' ActiveStorage.
Here lies the trap: By default, the active_storage_attachments table defines its record_id column as a bigint. It expects a number. When you try to save an attachment associated with a record that has a UUID string for an ID, the system has to make a choice. Instead of throwing an error, the database adapter (or Ruby itself, depending on the layer) attempts a cast.
In Ruby, string-to-integer casting is notoriously permissive. It parses until it hits a non-numeric character.
0210ae70... becomes Integer 210.e9540f96... (starting with a letter) becomes Integer 0.Since UUIDs are hexadecimal, a significant portion of all generated IDs begin with a letter (a through f). Every single one of those exports was saved into the database with a record_id of 0. The entropy of a 128-bit UUID was instantly crushed into a single digit.
Let's look at the implementation that caused this collision. The developers defined the model to use the UUID, but failed to update the underlying storage association to handle it.
The Vulnerable Model:
# app/models/decidim/private_export.rb
class PrivateExport < ApplicationRecord
# This implies id is a UUID string
include Decidim::HasUploadValidations
# The retrieval logic
def self.retrieve(user)
# Finds the record by UUID, but the attachment is linked by INT
user.private_exports.order(created_at: :desc).first
end
endThe Invisible Failure:
When ActiveStorage saves the attachment, it executes SQL similar to this:
INSERT INTO active_storage_attachments (record_id, record_type, ...)
VALUES ('e9540f...', 'Decidim::PrivateExport', ...);Because the column record_id is bigint, Postgres (or the driver) casts 'e9540f...' to 0.
The Dangerous Retrieval:
When a user clicks download, the controller looks up the export. Even if the controller finds the correct PrivateExport record (with UUID e9540f...), when it asks for the file, ActiveStorage queries:
SELECT * FROM active_storage_attachments
WHERE record_id = 0 -- collision!
AND record_type = 'Decidim::PrivateExport';If 50 users have exports starting with a letter, they all share ID 0. The database returns the first one it finds. You just downloaded someone else's life.
Exploiting this requires zero technical skill, just patience and a registered account. It is a game of "Data Roulette."
Step 1: The Setup An attacker creates an account on the target Decidim instance. They navigate to the "Download Your Data" section and request an export.
Step 2: The Trigger
The system generates a UUID for the export. The attacker doesn't control this UUID, but probability is on their side. There is a ~37.5% chance the UUID starts with a letter (a-f), causing it to map to ID 0.
Step 3: The Collection
The attacker polls the download endpoint. If their export mapped to 0, and a victim also requested an export that mapped to 0, the database now holds multiple attachments for ID 0.
Depending on the database sort order (usually insertion order or primary key), the attacker's request might serve up the most recent file added to ID 0. If the victim requested their export after the attacker, the attacker's subsequent download attempts could fetch the victim's ZIP file.
No SQL injection, no XSS. Just bad schema design and a download button.
The remediation for CVE-2025-65017 was drastic but necessary. You cannot simply "un-mix" the data. Once multiple files are associated with ID 0, there is no reliable way to know which file belongs to which UUID. The metadata linkage is severed.
1. Schema Change:
The patch involved a migration to change the PrivateExport primary key back to a standard auto-incrementing Integer (id) to satisfy ActiveStorage, while moving the UUID to a dedicated, separate column (uuid) for public-facing lookups.
2. The Purge:
The most painful part of the fix was the cleanup. A rake task (decidim:upgrade:clean:remove_private_exports_attachments) was released to physically delete all existing private export attachments. It was the only safe move: treat all cached data as compromised.
3. Scoped Lookup:
The controller logic was tightened to ensure that even if a collision theoretically happened, the query is strictly scoped to the current_user's association, though the schema change fundamentally solves the root cause.
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Decidim Decidim | >= 0.30.0 < 0.30.4 | 0.30.4 |
Decidim Decidim | >= 0.31.0.rc1 < 0.31.0 | 0.31.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-200 (Info Disclosure) |
| CWE ID | CWE-703 (Improper Check of Unusual Conditions) |
| CVSS v4.0 | 8.2 (High) |
| Attack Vector | Network |
| Privileges | Low (Any User) |
| Impact | Confidentiality Loss (Total for affected scope) |
The application implicitly casts distinct UUID strings to identical integers (often 0), causing object reference collisions.
An unauthenticated remote denial of service vulnerability exists in the Unleash feature management platform. By submitting a crafted JSON payload containing deeply nested structures to an OpenAPI-validated endpoint, an attacker can trigger uncontrolled recursion within the error formatting module. This leads to a call-stack exhaustion (RangeError: Maximum call stack size exceeded) inside the Node.js runtime, causing the service to crash immediately without recovery.
CVE-2026-63004 is a server-side request forgery (SSRF) vulnerability in the Unleash feature management platform. Authenticated administrators with CREATE_ADDON or UPDATE_ADDON privileges can exploit this vulnerability to initiate requests to loopback addresses, private networks, and cloud metadata endpoints, potentially leading to information disclosure and credential extraction.
Prior to version 8.0.3, Unleash's Markdown event formatter directly mutated the global template-escaping function of the shared mustache Node.js module, resulting in a process-wide security degradation where HTML/Markdown escaping was permanently disabled for the application lifetime.
A critical SQL injection vulnerability exists in the GeoTools open-source Java library. This vulnerability is situated within the post-processing phase of OGC Filter conversion inside the PostGIS DataStore module. Specifically, the `jsonArrayContains` function does not validate or sanitize its arguments before constructing PostgreSQL SQL/JSON path evaluation queries. An unauthenticated remote attacker can exploit this weakness by submitting crafted filters via standard OGC services like WFS or WMS to execute arbitrary SQL commands on the underlying database system.
CVE-2026-61824 is a high-severity Cross-Site Scripting (XSS) vulnerability in kepano/defuddle before version 0.19.1. Custom site extractors for platforms such as X/Twitter, Substack, and YouTube constructed HTML representations via template string interpolation without output escaping. This allowed malicious pages to bypass standard parser sanitization routines and execute arbitrary JavaScript.
A high-severity vulnerability exists in KeystoneJS, a popular Node.js CMS and GraphQL framework, where the query resolution engine fails to validate signed negative integers within the pagination subsystem. Unauthenticated remote attackers can leverage this flaw to bypass the 'graphql.maxTake' safety boundary. By sending large negative values in the 'take' query parameter, the underlying Prisma ORM interprets the value as an instruction to fetch rows from the end of the collection, allowing malicious actors to bypass pagination limits, trigger database resource exhaustion, and execute application-level Denial of Service (DoS) attacks.