Vulnerabilities

Nine Case Files Reach Two Civic Halls — Decidim's July Security Repairs

Decidim's nine disclosures of 13 July 2026 tell one connected story: credentials, records, private files, query parts, stored pages, and push destinations reached a sensitive last step without all the organization, actor, grammar, or destination context that step required.

A cutaway civic hall contains separate navy and teal organizations above a sequence of record, permission, query, publishing, and delivery desks, with nine sealed case files arranged below.
In this article

Research basisReview of the nine CVEs disclosed on July 13, their official releases, advisories, repair pull requests, and a two-organization defensive regression model

SourceDecidim Security Team / Decidim source and pull requests / SOSEC source review

1 Nine envelopes arrived together, but they did not contain the same defect

1.1 The 13 July desk held nine different failures and one shared question

On 13 July 2026, an operator following Decidim's security page did not receive one dramatic bulletin. Nine advisories appeared after the project's disclosure grace period, each pointing to a different room of the platform: questionnaires, identity verification, organization-user search, exports, census administration, API identity, rich-text publishing, and Web Push. Read separately, they look like a crowded release note. Laid side by side, they form a case about information losing its escort on the final few steps.

Decidim is a Ruby on Rails framework for participatory-democracy services. One installation can serve more than one organization, and each organization can have its own hostname, participants, administrators, participatory spaces, verification records, content blocks, API credentials, and operational choices. Sharing an application and database is part of the design. The safety requirement is that every sensitive operation still knows which civic hall it is serving and which person is standing at the desk.

This report follows public project evidence only. We reviewed the three fixed release objects, all nine GitHub Security Advisories, the project's security policy, and the repair pull requests named by those advisories. We did not query a production Decidim installation, inspect a third party's identity documents, or send a test request to an external push service. The release objects identify which release lines carry the repairs, each GHSA defines its own range and severity, and the pull-request diff shows where the check enters the implementation; none substitutes for the others. Where the report joins several patches under one engineering lesson, that connection is our source-backed synthesis, not a vendor umbrella name for the disclosure, so every impact statement still returns to its own advisory and preconditions.

The nine files are CVE-2026-45086, CVE-2026-45330, CVE-2026-45376, CVE-2026-45377, CVE-2026-45378, CVE-2026-45414, CVE-2026-45415, CVE-2026-45572, and CVE-2026-45573. Two carry High severity in the public advisories: the reusable verification-document link and the organization-crossing JWT/API identity. The remaining seven are Medium. Severity tells an operator which source paths deserve urgent review, but local impact still depends on whether the relevant module is enabled, whether the path needs an ordinary participant or an administrator, whether reusable links or stored objects exist, and where the database and workers sit on the network. The files become easier to hold when each is named by the last operation that needed more context:

Public fileLast sensitive operationMissing decisionPrimary repair
CVE-2026-45086Edit demographics questionnaire questionsMay this actor update this component subject?PR 16665
CVE-2026-45330Open or decide a pending verificationDoes this record belong to the active organization and handler?PR 16666
CVE-2026-45376Build the organization-user search orderIs every request value still data inside a closed query grammar?PR 16668
CVE-2026-45377Deliver a private personal-data exportMay this signed-in person receive this exact file now?PR 16680
CVE-2026-45378Render an identity-verification imageMay this participant or same-organization administrator view it now?PR 16680
CVE-2026-45414Resolve an API or JWT-backed userDoes the principal belong to the organization selected by the host?PRs 16673 and 16756
CVE-2026-45415Create, update, list, or remove CSV census dataDoes the actor have the authorization permission for this action?PRs 16674 and 16703
CVE-2026-45572Render stored administrator-authored HTMLHas active markup been removed at the output cell?PR 16451
CVE-2026-45573Send a Web Push request to a stored endpointIs the destination approved at the moment of network use?PR 16714

The table does not flatten the defects into “nine tenant bugs.” The SQL path is about query grammar. The stored HTML path is about output encoding and sanitization. The push path is about outbound destination control. What connects them is placement: a check existed elsewhere, or an object had already passed through a trusted layer, yet the function about to read data, change state, send bytes, interpret syntax, render markup, or open a connection lacked one fact it needed.

That placement explains why several reassuring facts can coexist with a real flaw. A user can be authenticated while carrying a principal from another organization. A blob URL can be cryptographically signed while no longer consulting the current visitor. One WHERE predicate can use binds while a neighboring ORDER BY expression interpolates the search term. A push endpoint can have been accepted months ago while remaining unsafe when a worker finally calls it.

The public sources establish vulnerable code paths and affected package ranges. They do not establish exploitation of a named installation, universal exposure of every dataset, or a common attacker campaign. Local response therefore starts with version and reachability, then advances only when identity, request, record, job, database, file, or egress evidence supports the next claim. That discipline protects both urgency and credibility.

For the rest of the story, imagine two controlled organizations on one test installation: the navy hall and the teal hall. Each has its own users, administrators, authorizations, content, exports, and credentials. The colors are fixture labels, not real municipalities or geographic claims, and every object is synthetic. Both halls travel through the same proxy, Rails application, database service, storage adapter, and worker topology so the test changes organization context without accidentally changing the whole system. Every repair will be asked the same practical question: when an object from the navy hall appears at a teal desk, which function notices, and does it notice before anything sensitive happens?

An archivist arranges nine sealed case folders on a wooden desk while a separate later folder remains in a side drawer.
Figure 1. The July set contains nine public case files. They belong on one review desk, yet each keeps its own affected range, prerequisite, impact, and repair evidence.

1.2 Three fixed release lines tell operators where the investigation starts

The first operational question is not “Do we run Decidim?” It is “Which Decidim packages, release candidates, forks, workers, and restored environments can execute one of these paths?” The release objects provide three convergence points: 0.30.9 for the seven repairs applicable to the 0.30 stream, 0.31.5 for the full July set on 0.31, and 0.32.0 for the repaired 0.32 release line.

Release lineFixed objectJuly scopeOperator reading
0.30v0.30.9Seven applicable repairsDemographics-question editing and the later organization/JWT behavior are not listed for this stream; still verify every installed package against its advisory.
0.31v0.31.5All nine July advisoriesUse 0.31.5 or a later supported release as the common target, then confirm every process loaded the same bundle.
0.32v0.32.0All nine July advisoriesRelease candidates before 0.32.0 appear in advisory ranges; the final 0.32.0 release carries the repairs.

Version 0.31.5 is the cleanest narrative center because its release notes name all nine security fixes together. It is still a bundle, not a magic number printed by the web tier. The release notes direct implementers through bundle update decidim, bin/rails decidim:upgrade, bin/rails db:migrate, and bin/rails data:migrate after the required prior patch line; record each command's artifact, output, and schema result instead of treating the tag as the procedure. The operator must verify the actual Decidim gems, source revision, container digest, migration state, and loaded worker image. A stale background process matters especially for stored push endpoints, where a request can be accepted by one process and used later by another.

Version 0.30.9 is narrower. Its release object contains the seven repairs that apply to that maintained stream; the demographics editor authorization and the organization/JWT issue begin in later code lines according to the release reconciliation. Copying a nine-row checklist onto 0.30 without this distinction creates false findings. Ignoring 0.30 because one advisory starts later misses the other seven.

Version 0.32.0 contains the July repairs and also mentions CVE-2026-44282, an item carrying a 6 September disclosure date. That file is outside this report's nine-case July scope. Keeping it separate avoids silently changing the incident set whenever a later release object accumulates another security note.

Each GHSA remains authoritative for its own affected package interval. Most list vulnerable ranges ending at 0.30.9, 0.31.5, or 0.32.0 and include release-candidate intervals where applicable. The demographics and JWT/API cases differ from the common pattern. A scanner or inventory query should therefore preserve package name and exact version, not reduce the whole installation to one top-level application string.

Decidim's security policy explains the timing. The project can publish a fixed release first, leave implementers a grace period, and disclose advisory details when that window closes; the policy assigns two months to ordinary cases and a longer interval to critical issues. A May deployment record and a July disclosure record therefore answer different questions. Keep four dates in the asset ledger—release publication, internal approval, actual rollout, and process restart—instead of using any one of them as a substitute for the others.

Keep immediate protection and historical evidence on parallel tracks. Immediate work upgrades the bundle, restricts risky functions during mixed-version rollout, and prevents old workers from claiming jobs. Evidence work preserves release manifests, authorization changes, custom content, push subscriptions, download access, query logs, and request-to-job correlations. Place preserved copies in an access-controlled evidence store, record collection commands, time ranges, and hashes, and keep document or subscription secrets out of the general incident channel. Cleaning stored objects before preservation may remove risk and also erase the reason a later tenant decision was made.

With the release lines pinned, the nine folders stop moving. The next step is to follow the first object through the hall: a credential that is perfectly valid in the navy organization and presented unchanged at the teal organization's host. The repair begins before any business resolver sees it.

2 The first key fit the lock, but the hostname belonged to the other hall

2.1 An API key must select a user inside the organization that received the request

At the navy hall, a system administrator creates an API user and receives a key and secret. The pair is valid. Nothing about the characters in the key announces a hostname, and the same Decidim process may answer for the teal hall a moment later. Before the repair, presenting the navy pair to the teal host could let authentication begin with a globally selected API user. The credential had proved that somebody knew a secret, but the lookup had not yet proved that the selected principal belonged behind this particular front door.

This is the important distinction in CVE-2026-45414: the defect did not require a forged credential. The dangerous object was an authentic credential used under the wrong organization context. Treating that situation as an ordinary password failure misses the architecture. In a multi-organization service, identity is not merely a user row; it is a user row resolved under the organization chosen from the request. The hostname arrives before the key lookup, so the finder has enough information to make that choice if the information is carried all the way in.

Pull request 16673 makes that carriage explicit. The Devise strategy changes its call from find_for_api_authentication(api_key: key) to find_for_api_authentication(api_key: key, env: env). The model then reads decidim.current_organization from the Rack environment. If that value is absent, the finder returns no user. If it is present, the authentication query contains two facts together: the supplied api_key and the active organization's id in decidim_organization_id.

The placement matters more than the number of changed lines. A later controller check would run after Devise had already materialized a principal and possibly after middleware had associated that principal with a request. By narrowing the database lookup itself, the repair ensures that an API user from the navy hall never becomes the provisional identity of a request to the teal hall. Authentication cannot succeed first and be corrected later; the wrong organization produces no resource for the secret validator to accept.

The upstream request specifications tell the same story without spectacle. They switch the test host to another organization and expect sign-in to return 403 Forbidden. They also create an API user in that other organization with the same key but a different secret, then confirm that the original pair still fails. In both cases the response must omit an Authorization header and must not contain a JWT token. Those negative assertions matter: a rejected body is not enough if a usable credential leaked through a header.

Once the fixed finder is loaded everywhere, the navy key stops at the teal entrance. But a second path remains inside the building. An API request can already carry a signed JWT or an established user session, and GraphQL still needs to decide which user it exposes to resolvers. The next patch repeats the organization question at that later handoff instead of assuming the earlier door was the only one.

2.2 A signed JWT still needs an organization check when GraphQL builds its user context

Imagine that the navy credential was used correctly at the navy host and a JWT was issued there. The token is not corrupted when it is copied into a request to the teal host; its signature can remain valid. That cryptographic fact answers who issued the token and whether its contents changed. It does not, by itself, answer whether the user represented by the token may become the current API user for the organization now serving the request.

Before pull request 16756, the API query controller's api_user method returned current_api_user || current_user. Either source could therefore supply a real user object before the controller compared its decidim_organization_id with current_organization.id. A GraphQL query such as the session query could observe a user context that belonged to another organization. The public evidence supports that cross-organization session behavior; it does not justify assuming that every resolver or every installation exposed the same fields.

The repair adds a small gate named organization_user. The candidate remains current_api_user || current_user, but it is returned only when a candidate exists, a current organization exists, and the two organization identifiers are equal. Otherwise the helper returns nothing. The controller memoizes that filtered result, so downstream context construction receives either a same-organization user or no API user at all.

Upstream tests exercise both ways a candidate can arrive. A controller test signs in a user from another organization, posts a GraphQL session query, and expects session to be null. A request test obtains an authorization value in one organization, switches the host to another, sends the same value to /api, and again expects a null session. The rejection occurs at context creation, before a resolver can treat the foreign user as local.

The API-key patch and the JWT patch are related but not interchangeable. The first narrows the credential-to-user lookup. The second filters an already materialized user before GraphQL exposes it. Keeping both checks is deliberate defense at two different handoffs: one covers API sign-in, while the other covers the query context produced from API or regular session state. Removing the later check because sign-in is fixed would assume that every future candidate can only enter through that one strategy.

A hand-drawn split civic counter shows a navy API key and JWT being checked against the navy organization before a teal counter refuses them.
Figure 2. A credential can be authentic and still be wrong for the receiving organization. The repaired API-key finder and GraphQL user context each compare identity with the host-selected organization at their own handoff.

3 A record number opened a drawer; permission decides whether the hand may reach inside

3.1 Verification, questionnaire, and census actions now ask their policy at the action itself

The next morning in the test hall, a teal administrator receives a URL containing the numeric ID of a pending identity-document authorization. The number is well formed, and a row with that primary key exists. The trouble is that the row was created for a navy participant. A global Authorization.find(id) can answer the database question perfectly while answering none of the civic one: which organization owns this pending case, which verification workflow is handling it, and is it still pending?

CVE-2026-45330 lived in that gap. The confirmation and rejection controllers for ID documents, and the postal-letter postage controller, loaded a pending authorization by global primary key. An administrator was signed in, but the record loader did not make the active organization part of object selection. Possession of another row's identifier could therefore bring an out-of-organization authorization into an administrative workflow that was built for the current hall.

Pull request 16666 introduces PendingAuthorizationLoader. Its load_pending_authorization! method starts from an Authorizations query with three constraints: organization: current_organization, the expected handler name, and granted: false. Only after that relation is built does it call find(pending_authorization_id). The numeric ID remains an input, but it can select only a record already inside the permitted set.

The handler name is not decorative. An ID-document confirmation route should not accidentally operate on a postal-letter authorization merely because both objects share a table and a pending state. The granted: false condition likewise prevents a workflow intended for undecided cases from treating a completed authorization as pending. Organization, workflow type, and state become part of the lookup's meaning; post-load checks arrive too late to define which object was selected.

The repaired tests create another organization, create a pending authorization for a user there, set the request's current organization to the first hall, and sign in a local administrator. Requests to open a confirmation, submit a rejection, or process postal-letter postage raise ActiveRecord::RecordNotFound; the system test presents the same result as a 404 page. This is useful negative behavior: the response neither confirms that the foreign record exists nor permits the action to reach its command.

Two other July files concern missing action authorization; their record loaders were not the source of the defect. In CVE-2026-45086, the shared HasQuestionnaire concern already enforced :update permission for ordinary edit and update, but the question-management actions needed the same enforcement. Pull request 16665 places enforce_permission_to(:update, permission_subject, questionnaire:) in both edit_questions and update_questions, and uses the same overridable subject for the other edit paths.

That overridable subject lets one shared concern serve different components without pretending their policies are identical. Its default is :questionnaire; the demographics questions controller returns :demographics. The demographics permission tests allow an organization administrator and deny a regular user, while shared system examples also preserve the intended component-admin behavior elsewhere. The repair is therefore not a hard-coded role check pasted into a view. It asks Decidim's permission system about the actual action and subject.

CVE-2026-45415 follows the same action-level pattern across two pull requests. PR 16674 enforces authorization permissions for CSV census index, destroy, the import form, import creation, and the instruction view. During review, PR 16703 closes the neighboring record workflow by enforcing :create on new and create, and :update on edit and update. Reading only the first patch would leave the operator with an incomplete map of the fixed surface.

Upstream tests use a process administrator as the denied actor and an organization administrator as the allowed actor. That distinction is operationally important. “Admin” in a modular civic platform is not one universal capability; a person may administer a participatory process without having authority to manage organization-wide verification data. A local test should create the exact roles it deploys, exercise each action, and confirm both response and database side effects. A redirect or 404 is only half the proof if a row still changed.

3.2 A signed file reference identifies the object; a live policy decides who receives its bytes

A different record leaves the database and enters a message. A participant asks Decidim for a personal-data export, or uploads an image for identity verification. A link is later copied from an email, a browser download list, or an administrator's review page. With an ordinary Active Storage blob URL, whoever possesses a still-valid address may be able to present it directly to the storage-serving path. The application that knew the participant and organization when it drew the page is no longer standing at the final download.

CVE-2026-45377 covers private exports; CVE-2026-45378 covers verification documents and carries the higher public severity. Their details differ, but PR 16680 gives them a common delivery mechanism. Decidim stops handing the protected cases straight to an unmediated blob address and introduces a PrivateDownloadsController that requires a signed-in user, resolves a signed reference, asks a record-specific authorization method, and then streams the file through Rails with send_data.

The new PrivateDownload token is deliberately narrow. A JSON-backed ActiveSupport::MessageVerifier signs a payload containing the record's GlobalID and the requested attachment name, under the purpose :private_download. Purpose separation prevents the same signed data from being accepted as an unrelated kind of token. The attachment name matters because one model may expose more than one attachment, and policy needs to reject names it did not intend to serve.

A valid signature is only the first checkpoint. from_token verifies the purpose, locates the GlobalID, and turns missing records or invalid signatures into InvalidTokenError. The controller then checks that the requested attachment is actually attached and calls authorized_for?(current_user). If any step fails, the response is not found. Only after all of them succeed does the controller download the blob and choose inline disposition for images or attachment disposition for other content.

The policy lives with the underlying record because “private” has different meanings. A PrivateExport accepts only when its attached_to user is the current user and the requested attachment is file. An Authorization accepts its owner, or an administrator whose organization matches the authorization's organization, and only for verification_attachment. An Attachment accepts only the file attachment and delegates the human decision to can_participate? for its restricted space.

This division of labor is what makes the redesign stronger than replacing one opaque URL with another. The signed token says, “these bytes are associated with this record and attachment name, and the reference was not altered.” The record policy says, “this person may receive them now.” If a link moves from the navy participant's browser to a teal account, the signature can remain valid while the policy returns false. Integrity of the reference does not become authorization of the visitor.

The upstream controller tests make the distinctions visible. A private export is served to its owner and rejected for another user. A verification image is served to a same-organization administrator and rejected for a regular user who does not own it. A PDF from a restricted participatory process is served to a member and rejected for a nonmember. An invalid token is rejected. These are separate policy branches, not one generic “logged in” check.

Serving through Rails does impose a data path and performance cost, which the pull request itself acknowledges. That is a capacity-planning concern, not a reason to bypass the controller for sensitive files. Measure concurrent large downloads, slow clients, proxy buffering, range behavior if required locally, memory pressure, storage latency, worker or web-thread occupancy, and error behavior with synthetic files. Set capacity and timeout alerts before rollout, then confirm any CDN or acceleration design still invokes an equivalent current-user policy before minting a short-lived delivery. Scale the protected route or choose a storage mechanism that can preserve equivalent application authorization; do not quietly restore a reusable bearer link during optimization.

Evidence collection should record the private-download purpose, record type and identifier, attachment name, actor and organization identifiers, policy result, response status, and byte count where appropriate. It should never record document contents or a reusable full URL. Compare application authorization events with origin, object-store, and CDN access records: a controller-side 404 proves the repaired route rejected the visitor, but it does not by itself prove that a previously cached or direct storage address returned no bytes. If historic blob URLs were exposed in emails, logs, or tickets, upgrading changes future link generation but may not revoke every old storage address automatically. Inventory storage exposure and retention separately, apply provider controls alongside application policy, and do not assume the new controller erases the past.

A hand-drawn records room shows a signed file ticket passing through a current-user permission desk before a sealed document leaves the correct organization cabinet.
Figure 3. The reference selects an exact record and attachment; the live record policy decides whether the current person may receive the bytes. Both checks are required at download time.

4 The filter was parameterized; the sorting expression was quietly writing its own SQL

4.1 The dangerous term crossed into ORDER BY, beside predicates that looked safe

The story then leaves identity records and arrives at a small autocomplete box in the administrative interface. An administrator begins typing the name, email, or nickname of an existing participant to add to a conference or meeting. The visible task is ordinary: return matching users, rank the closest names first, and let the form store a selected ID. Underneath, PostgreSQL's trigram similarity functions were being asked to turn the search term into a ranking expression.

In the removed OrganizationController#users action, the relation itself began sensibly. A nickname search used where("nickname LIKE ?", "#{nickname}%"). Name and email searches used ILIKE ? with bound values and combined their relations. If a reviewer stopped at those predicates, the code looked parameterized. But ranking was constructed on the next line, and SQL has more input surfaces than WHERE.

For nickname searches, the controller interpolated the nickname into a string containing similarity(nickname, '...') DESC. For other searches it built GREATEST(similarity(name, '...'), similarity(email, '...')) and an averaged similarity expression the same way. The already-composed strings were passed through sanitize_sql_array and then wrapped in Arel.sql before reaching order.

Those names sound reassuring, but their position is decisive. There was no placeholder array left for Active Record to bind; interpolation had already merged request data with SQL grammar. Arel.sql then tells the query builder that a fragment is intentionally SQL. A helper cannot reconstruct the lost boundary between literal data and executable syntax after both have become one string. CVE-2026-45376 is therefore an ordering-expression injection, not evidence that the neighboring bound predicates failed.

The public advisory describes a blind, time-based path in the administrative organization-user search. That means a manipulated term need not make database rows or errors visible to create an observable effect; controlled delay can become the signal. Access to the affected administrative workflow is a prerequisite in the published case. It narrows who can initiate the request, but an administrative account is also close to personal data and high-value workflows, so the prerequisite should calibrate triage without erasing the risk.

Validation uses a disposable database populated with synthetic users, an application-only test role, and an explicit query timeout. Compare an ordinary term with a harmless quote and metacharacter corpus, inspect generated statements or bound-parameter telemetry, and destroy the database after each run. The acceptance result is structural: request values remain parameters or are processed by a supported search layer; they never appear as new functions, operators, comments, or clauses in logged SQL, and a request merely completing without error is not accepted as proof.

Historical review should start with reachability. Determine whether the removed JSON route existed in the deployed release and whether reverse-proxy, Rails, or database logs retain calls to it. Correlate authenticated administrator, organization, request ID, term length, response time, database role, and statement fingerprint, then establish normal autocomplete p50 and p95 baselines segmented by time and data volume. For an individual slow request, compare lock waits, connection-pool pressure, database load, and the statement duration on the same trace. Long response times alone do not prove injection; maintenance, lock contention, or slow similarity scans can look alike. Escalate only when unusual input shape, a stable delay signal, and query evidence agree.

The database role sets the ceiling for consequence. Record whether the Decidim connection could only read and write application tables or also held schema, extension, file, or administrative privileges. Review installed PostgreSQL extensions, callable functions, search paths, statement timeouts, and audit settings as well as grants; a role can inherit powerful behavior through its environment even when its name looks ordinary. Review row-level controls if a deployment adds them, and identify whether multiple organizations share the same role and schema. Repeat the check in preview, reporting, and disaster-recovery databases, where production-like data may coexist with looser roles. Least privilege does not repair query construction, but it converts an uncertain software path into a more bounded operational risk while the upgrade proceeds.

The lesson for review is unusually concrete: inspect every grammar-bearing method, not just filters. In Rails that includes order, reorder, select, group, joins, fragments passed to Arel.sql, and database-specific functions. A safe where on the line above is not inherited by its siblings. Each value must either remain data in a bind or be chosen from a closed application-controlled set.

4.2 The repair removes the bespoke route and sends autocomplete through the supported GraphQL search

PR 16668 does not try to salvage the similarity strings with another escaping call. It removes the users action from the organization controller and deletes the member route that exposed it. The form builder stops accepting and serializing a per-field searchURL. Conference and meeting forms no longer point at the retired endpoint. This is a meaningful reduction: the unsafe parser path disappears instead of remaining as an alternate interface that callers might rediscover.

The browser-side autocomplete now obtains Decidim's configured api_path and sends a JSON POST containing a GraphQL users query with a wildcard filter. The changed JavaScript escapes backslashes and double quotes before inserting the typed value into the GraphQL document, reads data.users, and maps each returned user to an ID and human label. If the request fails, the component returns an empty suggestion list; the removed JSON route is no longer a fallback.

Moving to GraphQL is not a magical property that makes arbitrary strings safe. The security value in this patch is that the particular raw similarity ORDER BY construction and its route are gone, while autocomplete converges on Decidim's supported user-search implementation. The supported resolver must still retain its authentication, organization filtering, complexity limits, and query-construction tests; changing the interface name does not retire those safeguards. Operators should test and adopt the fixed code as a unit. Copying only the JavaScript while leaving the old route callable would preserve unnecessary attack surface; deleting only the route while a custom form still depends on it would break administration and invite a hurried restoration of the old controller.

The upstream diff also reveals where local forks may diverge. It removes searchURL examples from the form builder, adjusts system tests to provide api_path, and updates conference and meeting pickers. Search custom engines, themes, and overrides for users_organization_url, searchURL, the old controller action, or copied similarity ranking code. A route can survive in a downstream engine even when the core route is gone.

Regression begins with normal work. From each affected administrative form, type three characters, select a synthetic participant from the current organization, submit the form, and confirm that the intended ID is stored. Verify that blocked, managed, deleted, or other-organization fixtures remain absent according to the supported resolver's policy, so the migration does not trade SQL safety for a broader participant list. Exercise names with apostrophes, backslashes, double quotes, Unicode, combining marks, percent signs, underscores, and long but reasonable input. The point is to preserve useful civic names while verifying that unusual text remains data throughout the request chain.

Then prove retirement. A request to the former organization-users route should not invoke the old search action, and no client bundle should contain its URL. Database statement telemetry during GraphQL search should show stable statement shapes and parameterized values produced by the supported resolver. If local GraphQL customization introduces dynamic sorting, review that code independently; convergence helps only when the destination remains maintained.

The archivist can now close the SQL folder with a precise statement: the repaired release removes a bespoke administrative endpoint whose ranking strings mixed request data with SQL syntax, and routes the feature through the supported GraphQL search. That is narrower and more useful than saying “input is sanitized.” It tells reviewers exactly which construction must be absent and which user journey must still work.

A hand-drawn workshop separates user search cards from fixed SQL grammar pieces while an unsafe handwritten ordering strip is removed from the bench.
Figure 4. A bound filter does not protect a separately constructed sort. The repair removes the raw ranking route and keeps request text on the data side of the supported search path.

5 Some input waited in storage until a page rendered or a worker opened the network

5.1 Administrator-authored HTML is sanitized when the final cell turns it into a page

The fifth folder looks quiet because nothing happens when its content is saved. An administrator arranges an HTML block on an organization homepage, writes a static-page summary, or fills the left and right columns of a two-pane section. The text enters settings and may sit there through backups, translations, migrations, and release changes. The security event begins later, when a visitor opens the page and a view cell decides whether the stored string is text, approved rich content, or executable browser markup.

CVE-2026-45572 concerns four such rendering cells. HtmlCell#html_content, StaticPage::SummaryCell#content, StaticPage::SectionCell#content, and both column methods in TwoPaneSectionCell obtained a translated setting and called html_safe. In Rails, that call does not inspect the markup and pronounce it harmless. It marks the string as safe for insertion, telling the template layer not to escape it.

Administrator-only authorship changes the threat model but does not make the browser consequence disappear. An administrator account may be compromised, a database row may be restored from an older source, content may arrive through an import or local extension, or a role may be broader than the operator assumed. Once stored active markup is rendered on a popular civic page, it executes in visitors' origin context. The public case is stored HTML execution; local impact still depends on which cells are used, who could write them, and what browsers received.

PR 16451 replaces the five html_safe calls with decidim_sanitize_editor_admin. That is Decidim's sanitizer for administrator-authored editor content. Crucially, the call remains in the cell method after the translated value is selected. Every rendering pass therefore evaluates the content that will actually enter the page, including values stored before the upgrade. The repair does not rely only on a cleaner form submission that historic rows could bypass.

Sanitization is not the same as flattening every page to plain text. The added cell tests preserve supported structures: a paragraph with <strong>, emphasis with <em>, and a summary link still render. Separate fixtures include a script element or event-handler attributes such as onclick, onerror, and onmouseover; the resulting markup retains the safe text while omitting the executable tag or attribute. That paired test protects both safety and editorial function.

Multilingual settings deserve explicit fixtures. A safe English value does not prove that a previously imported Catalan, Spanish, French, or Chinese value passes through the same method, and a fallback locale can select content that the editor did not just display. Test a missing current locale, fallback to the default locale, a translation key that exists but is empty, and two-pane columns carrying different locale values. Populate every enabled locale with benign formatting and a nonexecuting marker that represents disallowed markup, switch locales, and inspect both the final DOM and actual response HTML; neither the database string nor an intermediate sanitizer return value can reveal what the browser ultimately receives.

Historic review should enumerate content blocks by manifest and scope, not run a blind search-and-delete over every angle bracket. Preserve a database snapshot or exported evidence under access control, identify the affected cell types, render them through the repaired code in a controlled environment, and record what the sanitizer changes. Hash the source row and rendered artifact, capture a DOM-level diff for removed tags or attributes, and map each block to its public URL, locale, publication interval, and available access logs. That produces a reviewable population without redistributing the active content. Destructive cleanup before preservation can remove a browser hazard while also erasing when the content appeared, which administrator last changed it, and which pages may have served it.

Local themes and cells can reopen the same path without touching the repaired files. Search overrides for html_safe, raw, direct calls to safe_join, or custom helpers applied to translated settings and editor fields. Review the last expression that returns markup, because an earlier sanitizer can be bypassed when a later helper concatenates new attributes or strings. Add the representative cell fixtures to the extension's own test suite, because core tests cannot cover code that the deployment has overridden.

Content Security Policy remains useful as an independent browser control, especially for reducing the reach of an unexpected script path, but it should not become evidence that unsafe markup is acceptable. Measure the real policy, nonces, allowed script sources, and reporting behavior; do not infer protection from a header name alone. The primary repaired behavior is simpler: active constructs are removed before the cell emits HTML, while supported editorial markup survives.

5.2 A Web Push endpoint is checked when written and checked again when a worker is about to use it

The neighboring folder contains no HTML at all. A browser asks to receive push notifications and submits an endpoint together with Web Push key material. Decidim stores the subscription under the user's notification settings. Hours or weeks later, a background job selects that record, builds a notification payload, and passes the endpoint to the Web Push library. The person who supplied the URL and the process that opens the connection may never share a request, log file, container, or deployment version.

CVE-2026-45573 arises from that delayed handoff. Without destination validation, a client-controlled endpoint can influence a server-side outbound request when a notification is sent. The public advisory classifies the result as server-side request forgery. Reach and consequence depend on the worker's network, proxy, DNS, metadata-service protections, and credentials, so a responsible review maps egress before claiming that a particular internal service was reachable.

PR 16714 adds a shared PushSubscriptionEndpointValidator. It rejects a blank value, parses the URI, requires a true HTTPS URI, normalizes the hostname to lowercase, and rejects a missing host. Anchored patterns then match push.services.mozilla.com, fcm.googleapis.com, android.googleapis.com, push.apple.com, opera.com, and notify.windows.com. Each expression accepts the exact domain or a genuine subdomain ending at the dot-delimited approved name; a lookalike such as good.example.attacker.tld fails the match.

The first invocation sits in NotificationsSubscriptionsPersistor#add_subscription. An unsupported endpoint raises a dedicated error before the subscription map is updated. The controller converts that error into an unprocessable-content response with an unsupported-browser message, and the browser-side code turns off the toggle and shows the error. The upstream test confirms both halves: a supported FCM endpoint is stored, while an example.org endpoint returns the error and leaves subscriptions absent.

Write-time validation improves new state, but it cannot speak for rows already in the database. It also cannot guarantee that another importer, console task, or older process did not write a record. The repair therefore includes the validator in SendPushNotification. Immediately before payload construction, the service filters user.notifications_subscriptions.values to supported endpoints. Only surviving subscriptions reach WebPush.payload_send.

The send-service test deliberately mixes one approved FCM subscription with one unsupported endpoint. It expects the Web Push library to be called exactly once, for the approved destination, and the returned response list to contain only that delivery. That is the crucial historic-state assertion: an unsafe value can exist in the fixture and still fail before network use. A migration that merely cleans today's database would not provide the same protection for a value reintroduced tomorrow.

An implementer may override the allowlist method to support another legitimate provider. That extension point is operationally useful, but it moves responsibility to the deployment. Document the exact domain family, business owner, vendor evidence, expiry or review date, DNS expectations, and network route before adding a pattern. Keep patterns anchored and dot-aware. A broad expression added to silence user complaints can turn a deliberate provider list back into arbitrary egress.

Application validation and network policy should agree without depending on each other. Permit background workers to reach the required push providers through an egress proxy or firewall, deny link-local and internal address ranges, and log destination host, resolved address where available, proxy decision, and request outcome. Resolution and connection can occur in different components, and a proxy may resolve the hostname again, so tests should correlate the same trace across application logs, proxy logs, and the actual connection. If an approved provider name resolves to an anomalous address, the network layer should still block it. The application decides what it intends to call; the network decides what the process is physically allowed to reach, and drift between the two configurations should alert.

Inventory is equally concrete. Extract endpoint hostnames without user key material, normalize and count them, separate approved families from everything else, and preserve the relationship to account, creation time if available, last delivery attempt, worker image, and organization under appropriate access control. Compare the observed host set with three independent sources: the validator patterns in the deployed artifact, the egress policy actually enforced for workers, and the documented provider list owned by operations. Any difference needs an owner and disposition, even when every individual hostname appears benign. An unknown hostname is a triage item, not proof of abuse. Correlate it with outbound proxy, DNS, job, and request evidence before drawing an incident conclusion.

The HTML and push repairs now meet at the same moment. Storage is not the end of validation. A value can be harmless while sitting in a row and dangerous only when a browser interprets it or a worker opens a connection. The last consumer must therefore repeat the decision in the vocabulary of its own risk: approved markup at render time, approved destination at send time.

A hand-drawn archive sends stored HTML through a page sanitizer and a stored push endpoint through a destination inspector before either reaches visitors or the network.
Figure 5. Stored values cross different last miles. One is interpreted by a browser, the other by a network client; each is checked again immediately before that interpretation occurs.

6 Two invented organizations can retell all nine cases without touching anyone's civic data

6.1 Build one fixture where every legitimate action has a deliberately wrong twin

The navy and teal halls now leave the metaphor behind and become a real test fixture. Create both organizations in an isolated Decidim environment with synthetic names and no copied production records. Give each an ordinary participant and an organization administrator; add a process administrator whose narrower role is important to the questionnaire and census cases. Use separate hostnames that traverse the same host-resolution middleware as the deployment, even if both resolve to a local test server.

In the navy organization, create one API user, obtain one API sign-in response, and establish one regular session suitable for exercising GraphQL. In both organizations, enable the relevant authorization handlers and create pending ID-document and postal-letter records. Add a demographics questionnaire, CSV census import and individual census record, private export, verification attachment, restricted participatory-space document, HTML and static-page content blocks, and a push subscription map.

Make identifiers easy to correlate in evidence but impossible to confuse in the application. Record the fixture seed, object type, database ID, organization ID, owner ID, role, handler name, and state in a local manifest. Do not force the two organizations to share primary keys or secrets; the security tests should succeed against ordinary database behavior. Reuse an API key only in the specific same-key/different-secret case from the upstream test.

Every case begins with its valid control. The navy API pair signs in at the navy host. The navy JWT produces a navy session there. A navy administrator reviews a navy pending authorization, an authorized administrator edits the demographics questions and census, the export owner downloads the export, a same-organization administrator views the verification image, and a restricted-space member receives the document. Normal autocomplete, safe rich text, and an approved push endpoint also work.

Only after the controls pass should the wrong twins run. Change one fact at a time: keep the credential and switch the host; keep the host and use the other organization's record ID; keep the file link and change the actor; keep the actor and revoke membership; keep the filter but introduce unusual text; keep the stored row but change the renderer or sender. A single-variable change makes a failed test explainable and prevents one early rejection from hiding a later missing check. The working matrix is compact enough to review before execution:

CaseValid controlWrong twinRequired observation
45086Organization admin edits demographics questionsRegular or process admin attempts the same actionDenied action; questionnaire unchanged
45330Local admin opens local pending verificationLocal admin supplies foreign pending IDNot found before decision command
45376Supported autocomplete returns local participantFormer route and unusual grammar charactersOld action absent; stable parameterized statements
45377Export owner downloads exact fileAnother signed-in user reuses link404 and zero bytes
45378Owner or same-organization admin views imageForeign or ordinary nonowner reuses link404 and zero bytes
45414Key and JWT work at issuing hostSame credential at other hostNo principal, header, token, or session
45415Organization admin manages censusNarrow process admin invokes each routeDenied; rows and imports unchanged
45572Supported editor markup rendersStored active tag or event attribute rendersSafe content remains; active construct absent
45573Approved provider endpoint reaches stubHistoric unsupported endpoint enters send jobUnsupported destination never reaches client

Run the table against the exact release candidate that will be deployed, not a developer branch that merely resembles it. If the service maintains 0.30, 0.31, and 0.32 streams, keep separate lockfiles and expected applicability for each. Label each skipped cell with the advisory range and package evidence that makes it inapplicable; an unexplained skip is indistinguishable from an unimplemented test. The 0.30 fixture should not manufacture the two cases that do not apply to that line, while all nine should be represented for the fixed 0.31.5 and 0.32.0 targets. Preserve results per line instead of letting the newest green build overwrite older maintenance evidence.

Workers belong in the same fixture. Queue a notification under one process image and execute it under the image intended for rollout; then reverse the order if mixed-version deployment is possible. Record the job serializer, queue name, enqueue time, digest of the process that claims it, and retry count. On every retry, verify that endpoint validation runs again immediately before sending and that the first filtering result was not frozen into the job arguments. Exercise generated private-download links through the same proxy and storage adapter used in staging. What matters is not only that the Ruby method passes in isolation, but that host, session, route, storage, job, and egress context survive the real handoffs.

The fixture should be disposable and repeatable. Seed it from code, generate fresh synthetic credentials on every run, capture versions and configuration, and destroy the database and storage objects afterward. Never import a production identity document “for realism.” A colored square or text file can prove attachment authorization just as well, without creating a second repository of sensitive civic information.

6.2 A rejection is proven by what did not happen after the response

An HTTP status is the first observation, not the verdict. A controller can return 404 after a command already changed a record, a background job can be enqueued before a redirect, and a proxy can replace an upstream response. For each wrong twin, define the forbidden consequence in advance: no selected principal, no authorization header, no session user, no state transition, no file bytes, no new census row, no unsafe DOM node, and no outbound client call.

Database telemetry helps distinguish lookup shape from final result. In the pending-authorization tests, confirm that the query contains organization, handler name, pending state, and ID; observing a missing row alone says nothing about how it was selected. In API authentication, confirm the key lookup also contains decidim_organization_id. For autocomplete, confirm the retired controller statement never appears and supported search values remain parameters. Statement fingerprints are often enough; avoid logging raw search text from real administrators.

Network tests need an observation point below the application service. Replace WebPush.payload_send with a strict test double or route staging egress through a controlled proxy. The unsupported historic endpoint must produce no client invocation and no DNS or proxy attempt. The approved provider-form fixture should produce exactly the expected call. This catches code that validates after the library has already resolved or contacted the destination.

Browser rendering needs the same last-step visibility. Inspect the produced DOM and relevant attributes after the cell renders, not merely the return value of a sanitizer helper. Confirm allowed formatting, links, and two-pane content survive. Confirm script elements and event-handler attributes are absent. If the deployment applies a custom Content Security Policy, keep that enabled for an integration pass but also test sanitization independently so one control does not mask the other.

Time should be treated as a test dimension for files and stored endpoints. Generate a private link, change the actor's relationship, and replay the link. Insert an unsupported subscription as if it predated the upgrade, then execute a post-upgrade send. Restore a pre-upgrade content row into the fixed build and render it. These cases demonstrate that the repair consults current policy at consumption, regardless of the date or process that wrote the object.

This laboratory cannot prove that an outside installation was exploited or clean. It proves that one identified build enforces the intended decisions under controlled conditions, and it reveals which production logs could answer historical questions. If a wrong twin fails, classify the observation before escalating it: incorrect fixture, deployment drift, missing instrumentation, a local override, or reproducible security behavior each leads to a different owner and next test. Preserve the failing trace and state snapshot before changing the environment. Keep that distinction in every report. Validation closes the engineering gate; incident assessment still depends on preserved local evidence.

A hand-drawn laboratory contains matching navy and teal civic-hall test benches, with credentials, records, files, pages, and push jobs crossing only through instrumented checkpoints.
Figure 6. Each valid action has one deliberately wrong twin. The instrumentation watches the principal, query, state change, file stream, DOM, and outbound call so a denial is more than a status code.

7 The response succeeds only when code, running processes, stored state, and evidence move together

7.1 A fixed version is the first release gate, not the last line of the rollout

  1. The first gate is artifact integrity. Build once from reviewed sources, resolve the expected lockfile, run unit and integration suites, generate an SBOM or equivalent package manifest, and sign or otherwise attest the image according to local practice. Connect the source commit, lockfile SHA, image digest, compiled-asset digest, and deployment manifest in one traceable chain, then pull the final image back from the registry and inspect that object rather than trusting only the CI workspace. Verify that the private-download controller, organization-scoped authentication, sanitizer, push validator, and retired search route are present in the built object, not merely in the branch from which somebody intended to build.
  2. The second gate is process convergence. Roll web and workers with a plan that prevents old processes from accepting new work after the database and assets have moved. A new web process can reject unsupported push subscriptions while an old worker still sends a stored one; a new asset bundle can call GraphQL while an old server still exposes the removed JSON route. A readiness probe should verify the actual build identity rather than only an open port, while worker heartbeats, consumer inventories, or startup logs should prove that old instances have departed. Drain queues where necessary, stop schedulers from racing the change, and record each process start time, loaded digest, and last job-claim time after rollout.
  3. The third gate is behavior. Run the two-organization smoke subset through the production-like ingress using synthetic organizations or a dedicated staging environment: wrong-host API key, wrong-host JWT session, foreign pending record, denied questionnaire and census actor, wrong-user file link, retired search route, sanitized historic cell, and skipped historic push endpoint. The same-host controls must work. A deployment that blocks every request is not a successful security repair.
  4. The fourth gate is recoverability and evidence. Take tested backups of database, application configuration, and static or object storage as the release notes recommend, but record how sensitive exports and verification documents are protected inside those backups. Define rollback criteria before the window. If rollback would restore vulnerable code, pair it with temporary route, role, file, push, and egress restrictions so availability pressure does not silently reopen every path.

During a mixed-version window, reduce optional exposure. Pause or restrict the administrative user pickers if the old route remains reachable, delay nonessential Web Push delivery, and avoid generating new private exports through an uncertain path. Such measures are time-bounded rollout controls, not substitutes for the fixed release. Record who enabled them, what they cover, and the condition that removes them.

Close the release gate only when every process reports the intended artifact, migrations have the intended state, synthetic controls and wrong twins have passed, old routes are absent, and monitoring can see the fields required for follow-up. Query build identity from every load-balancer backend and worker pool, compare it with the deployment manifest, and retain the result beside the smoke-test trace. “Package upgraded” is a valuable milestone. It is not yet proof that the worker holding yesterday's job or the pod behind one forgotten service has joined the same story.

7.2 Durable records must be preserved, examined, and remediated by type

Code rollout changes what happens next; it does not rewrite every trace of what happened before. Start a parallel durable-state workstream with its own owners and access controls. Preserve the minimum evidence necessary to answer historical questions, document hashes and collection times, and then remediate according to data classification and retention policy. Do not place identity documents, exports, raw tokens, or push key material into a general-purpose incident ticket.

For private files, inventory private exports, identity-verification attachments, and restricted-space documents created during the affected window. Identify how their links were delivered and whether storage or CDN access logs can associate requests with an address, time, cache result, and response. Distinguish an origin-signed address, a CDN-derived address, and the final download URL copied from a browser because their revocation mechanisms and log locations may differ. The new controller protects newly generated private-download routes, but a previously issued Active Storage address may have its own validity and cache behavior. Evaluate those old addresses with the storage provider; deploying the repaired application does not revoke them by itself.

Remove expired exports and obsolete verification documents under the organization's retention rules after evidence preservation. If a link-signing or storage secret must be rotated, model its effects on sessions, unrelated signed objects, caches, queued mail, and disaster recovery first. Rotation is not a ritual response to every signed-link defect; it is justified when it meaningfully invalidates exposed material and the service can absorb the collateral effects.

For Web Push, extract and normalize endpoint hosts while excluding authentication and encryption key values from broad reports. Classify hosts against the adopted validator, locate creation and last-use timestamps where available, and correlate unsupported entries with worker, DNS, egress-proxy, or firewall records. Preserve a suspicious row before deletion. An unfamiliar endpoint may be a stale browser integration, a local provider, test data, or abuse; the surrounding evidence decides.

For stored HTML, enumerate only the affected core cells and their local overrides first. Preserve revision and administrator audit information, render a copy through the repaired sanitizer, and review changes. Remove or correct active constructs in the source rows when policy requires, but keep legitimate formatting intact. A wholesale strip of all HTML can damage public information and still miss a custom renderer that marks another field safe.

For identities and records, review API keys and JWT usage by organization, pending verification decisions, demographics-question changes, census imports and record edits, and administrator role assignments during the available window. Look for mismatches among routed organization, candidate principal, record organization, handler, and actor role. Do not label every cross-host request malicious; mobile clients, proxies, tests, and administrator mistakes need contextual review.

For the SQL path, preserve application, proxy, and database logs before normal retention removes them. Search for calls to the retired route, unusual term shapes, repeated latency patterns, database errors, and statement fingerprints, then correlate them with authenticated administrator and organization. Avoid replaying logged terms against production. Use the isolated lab if a suspicious string needs structural analysis, and restrict access because search terms may contain names or email fragments.

Each state class ends differently: a file may be expired, an endpoint removed, a page sanitized, a key revoked, a role corrected, a record decision reviewed, or a log gap accepted as an uncertainty. Record the object population examined, selection query, evidence retained, action taken, exceptions, reviewer, and completion time. Include caches, replicas, backups, exports, and legal holds in the disposition: deleting the primary row may not remove every controlled copy, and some copies may require sealed retention under policy or law. One “database cleaned” checkbox would erase precisely the distinctions that made the nine repairs understandable.

7.3 Report eligibility, reach, evidence, and consequence as four different numbers

The first executive question is usually “How many organizations were affected?” A defensible answer starts by declining to compress four denominators. Software eligibility counts instances or packages in an advisory range. Feature reach counts organizations where a vulnerable component and prerequisite were available. Observed evidence counts requests, objects, or jobs that match review criteria. Confirmed consequence counts cases where independent evidence shows an unauthorized read, change, execution, or outbound connection. Publish each numerator with its denominator, as-of time, collection method, and known blind spots; a number without that frame will be copied into later briefings as if it described all four populations.

Internal communication should name the exact behavior under review: a foreign-organization principal, an out-of-scope pending record, a missing action policy, a reusable private address, a raw sort expression, active stored markup, or an unsupported outbound destination. Pair each statement with evidence class, observation window, confidence, and the next fact that could strengthen or weaken it. Concrete language helps service owners find logs and users understand any needed notice. It also prevents the two High advisories from inflating every Medium case into the same consequence or a single missing log from being presented as proof of compromise.

The four gates leave the operator with a result that can survive scrutiny: known code, known running state, known stored-state treatment, and claims proportional to evidence. That is slower than changing a version string and faster than revisiting the same incident months later because nobody can tell which worker ran, which old link survived, or what “affected” meant in the original update.

A hand-drawn civic operations room shows four connected gates for reviewed code, converged processes, durable-state review, and evidence-based reporting.
Figure 7. Version, process, stored state, and evidence are separate response gates. Closing all four prevents a patched web tier from hiding an old worker, historic object, or unsupported conclusion.

8 The durable lesson is carried by the function that performs the sensitive act

8.1 The last consumer needs the whole sentence, not a trusted fragment of it

At the end of the day, the archivist returns the nine folders to their shelf. They do not become one vulnerability, and the project did not publish them under one shared technical label. Yet the repairs can be remembered as one engineering sentence: the function about to authenticate, load, change, deliver, interpret, render, or connect must receive every fact required to decide safely at that moment.

For API sign-in, the sentence is “this key selects this API user inside the organization resolved for this request.” A valid key alone is a fragment. For the GraphQL context, it is “this candidate user belongs to the same organization the current host selected.” A signed JWT alone is another fragment. Both repairs move organization agreement into the function that hands identity to the next layer.

For pending verification, the sentence contains organization, handler, pending state, and record ID before the row is selected. For questionnaire and census work, it contains actor, action, policy subject, and target before the command runs. A primary key and an authenticated administrator each sound authoritative in isolation; neither describes the permitted operation without the rest.

For private files, the signed GlobalID and attachment name identify an untampered reference, while the current actor and record-specific policy authorize today's delivery. For user search, the term remains data while the application owns the query grammar. In both cases, “signed” or “sanitized” is too vague to review. The useful question is what property was established, where, and whether it is the property the final operation needs.

For content cells, stored authorship does not survive as permission to execute arbitrary browser behavior; the renderer admits the supported editor vocabulary. For Web Push, a previously stored URL does not survive as permission to open any connection; the sender admits supported HTTPS provider hosts. Time and asynchronous work make these repeats necessary, because the actor, software, policy, or stored value may have changed since the write.

This approach also exposes weak abstractions during code review. A method named find_by_token may return a global user when callers need an organization-scoped principal. A method named download_url may encode integrity but omit actor policy. A value marked html_safe may mean only “do not escape.” A string passed to Arel.sql may mean “trust this grammar,” even when part of it came from a request.

Make those distinctions visible in interfaces. Pass organization explicitly or retrieve it from a clearly defined request context; return organization-scoped relations and never globally loaded records; separate signed reference parsing from authorization; give policy subjects stable names; accept sort choices as enums or application constants; return sanitized rich-content types; and make outbound clients accept validated destination objects where practical. Default branches should reject unknown policy subjects, attachment names, sort keys, and providers rather than treating compatibility as permission. Any extension must arrive with its policy, tests, and observable decision fields. Types and names cannot replace tests, but they make an omitted fact harder to overlook and show reviewers which layer owns the security decision.

The synthesis is deliberately modest. It does not claim that every Decidim feature lacked organization isolation or that all nine defects share one exploit chain. It offers a way to keep nine specific repairs connected without distorting their prerequisites and consequences. Each folder remains independently sourced; the shared sentence helps a reviewer find the next place where context could be dropped.

8.2 Turn the July fixes into a closure ledger that future extensions must keep passing

A closure ledger begins with the public facts: nine CVEs, nine advisories, three fixed release lines, the relevant repair pull requests, and the date each source was reviewed. Beside every case, record the local package range, reachable feature, prerequisite actor, repaired function, valid control, wrong twin, observable forbidden consequence, durable-state population, telemetry fields, response owner, and evidence-based outcome. Link each conclusion to an immutable source revision, test-run identifier, and controlled evidence location. If a later decision changes a state such as “applicable” or “confirmed,” preserve who changed it, the supporting basis, and who reviewed the change. This is more useful than a generic “patched” column and compact enough to survive handover.

Add source-review checks where they are reliable. Flag a tenant-owned model loaded by bare find(params[:id]) inside an organization controller; calls that hand current_user into API context without organization comparison; protected attachments rendered as direct blob URLs; interpolated request data inside grammar-bearing Active Record methods; stored editor content marked safe; and outbound clients fed from persisted URLs. Each flag needs human review, not automatic accusation.

Review time-changing objects explicitly. Which links should remain usable after logout, role change, membership removal, object deletion, or organization transfer? Which stored endpoints should be rechecked before every job? Which content should be sanitized on write, render, or both? Which credentials may be replayed under another host? Write the answers as tests with a state change between creation and consumption.

When a release changes routes or centralizes a service, prove both adoption and retirement. Confirm every caller uses the new path, the old route is absent from every running process, compiled assets no longer reference it, and local forks did not preserve a copy. The autocomplete repair is a model: consolidation lowers review cost only when the alternate implementation is actually gone.

The final scene is intentionally uneventful. A navy credential reaches a teal host and produces no user. A teal administrator enters a navy record number and finds nothing. A copied file ticket yields no bytes to the wrong person. Odd search text stays text; historic markup loses active behavior; an unsupported endpoint never reaches the network. The ordinary actions still work in their own hall. That quiet, repeatable result is what closes all nine folders.

A hand-drawn civic archive shows credentials, records, files, queries, pages, and network destinations moving through a continuous chain of named checks into a signed closure ledger.
Figure 8. The long-term control is a continuous, testable sentence carried to each sensitive consumer and recorded in a closure ledger that local extensions must keep passing.

Research record

9Evidence, objects, and sources

The material below preserves the identifiers and references used in this report.

9.1Research objects

Products, actors, techniques, affected objects, and control points discussed in the report.

CVE setCVE-2026-45086; CVE-2026-45330; CVE-2026-45376; CVE-2026-45377; CVE-2026-45378; CVE-2026-45414; CVE-2026-45415; CVE-2026-45572; CVE-2026-45573

The nine Decidim advisories publicly disclosed on 13 July 2026

Fixed releasesDecidim 0.30.9; 0.31.5; 0.32.0

Convergence points for the applicable July repairs; reconcile each package with its own advisory

Request contextdecidim.current_organization

Rack environment value consumed by organization-scoped API authentication

Organization fielddecidim_organization_id

Compared during API-key lookup and GraphQL API-user context construction

Private-download purposeprivate_download

MessageVerifier purpose for GlobalID and attachment-name references followed by live authorization

Render controldecidim_sanitize_editor_admin

Sanitizer used by repaired HTML and static-page content cells

Egress controlPushSubscriptionEndpointValidator

HTTPS and approved push-provider hostname check at persistence and delivery

9.2Event chronology

  1. Core repairs merged

    The reviewed pull requests added action policies, scoped lookup, organization-aware identity, private delivery, sanitized rendering, and endpoint validation.

  2. Fixed release window opened

    Decidim published the repaired 0.30.9, 0.31.5, and 0.32.0 release objects under its disclosure policy.

  3. Nine advisories became public

    The project disclosed the nine CVEs, individual affected ranges, impacts, and repair references.

  4. SOSEC source review completed

    The release objects, nine advisories, and the public implementation diffs were reconciled into this defensive report.

9.3Sources and material

  1. Decidim v0.31.5 release: all nine July security advisories and upgrade instructionshttps://github.com/decidim/decidim/releases/tag/v0.31.5
  2. Decidim v0.30.9 release: seven applicable July security repairshttps://github.com/decidim/decidim/releases/tag/v0.30.9
  3. Decidim v0.32.0 release: July repairs and separate later-disclosure reconciliationhttps://github.com/decidim/decidim/releases/tag/v0.32.0
  4. Decidim security policy: supported versions and disclosure grace periodhttps://github.com/decidim/decidim/security/policy
  5. GHSA-vq6j-hj8w-7v39: missing questionnaire-editor authorizationhttps://github.com/decidim/decidim/security/advisories/GHSA-vq6j-hj8w-7v39
  6. GHSA-86fh-w43w-338c: cross-organization verification-record accesshttps://github.com/decidim/decidim/security/advisories/GHSA-86fh-w43w-338c
  7. GHSA-jvqq-cvh4-xm37: organization-user search SQL injectionhttps://github.com/decidim/decidim/security/advisories/GHSA-jvqq-cvh4-xm37
  8. GHSA-767h-63j4-5226: reusable private-export linkshttps://github.com/decidim/decidim/security/advisories/GHSA-767h-63j4-5226
  9. GHSA-q79h-67vx-m9xg: missing CSV census authorizationhttps://github.com/decidim/decidim/security/advisories/GHSA-q79h-67vx-m9xg
  10. GHSA-3mvf-82qp-8qh5: reusable verification-document linkshttps://github.com/decidim/decidim/security/advisories/GHSA-3mvf-82qp-8qh5
  11. GHSA-r3v7-5x4c-c69q: JWT-backed user exposed across organizationshttps://github.com/decidim/decidim/security/advisories/GHSA-r3v7-5x4c-c69q
  12. GHSA-533c-2vh9-4r86: stored HTML execution in content cellshttps://github.com/decidim/decidim/security/advisories/GHSA-533c-2vh9-4r86
  13. GHSA-2g9c-vf8h-prxx: stored Web Push endpoint SSRFhttps://github.com/decidim/decidim/security/advisories/GHSA-2g9c-vf8h-prxx
  14. Decidim pull request 16665: questionnaire action permission and demographics subjecthttps://github.com/decidim/decidim/pull/16665
  15. Decidim pull request 16666: organization-scoped pending-authorization loaderhttps://github.com/decidim/decidim/pull/16666
  16. Decidim pull request 16668: remove raw organization-user search and adopt GraphQL autocompletehttps://github.com/decidim/decidim/pull/16668
  17. Decidim pull request 16673: bind API-key authentication to current organizationhttps://github.com/decidim/decidim/pull/16673
  18. Decidim pull request 16674: enforce CSV census controller permissionshttps://github.com/decidim/decidim/pull/16674
  19. Decidim pull request 16680: private-download controller and live record policyhttps://github.com/decidim/decidim/pull/16680
  20. Decidim pull request 16714: validate Web Push endpoints at persistence and sendhttps://github.com/decidim/decidim/pull/16714
  21. Decidim pull request 16756: filter GraphQL API user by current organizationhttps://github.com/decidim/decidim/pull/16756
  22. Decidim pull request 16451: sanitize HTML and static-page content cellshttps://github.com/decidim/decidim/pull/16451
  23. Decidim pull request 16703: enforce CSV census record create and update permissionshttps://github.com/decidim/decidim/pull/16703