Let strong parameters authorize a model with no allowlist - #1717
Conversation
Ransack discarded the `permitted?` flag: the first thing `Search#initialize` did was `to_unsafe_h`, so strong parameters filtered keys but never authorized them, and every model needed its own allowlists even when the controller had already said exactly what may be searched (#1403). `config.strong_parameters = true` (off by default) makes a search built from permitted `ActionController::Parameters` skip `ransackable_attributes`, `ransackable_associations` and `ransortable_attributes`; every permitted key that names a real column, ransacker, alias or association is searchable, and the form helpers list the same. `ransack(params, strong_parameters: true / false)` overrides it for one search in either direction. What does not change: `ransackable_scopes` is consulted whatever the params, a plain Hash or unpermitted params keep the model allowlists, and a key naming nothing is dropped or raises under `ransack!` as before. - Search#initialize reads `permitted?` before the unwrap and resolves it with the option and the config into `Context#strong_parameters` - Context routes the three attribute/association checks and the three form-helper lists through one pair of private lookups that pick the model list or the `authorizable_*` existence list - spec/ransack/strong_parameters_spec.rb covers the default, the config, the per-call override both ways, unpermitted params, a plain Hash, existence, scopes, sorting, associations, the form-helper lists and the `permit(q: {})` wholesale case - docs: a subsection under Authorization with what to permit and the `permit(q: {})` warning, a Configuration entry, and an Upgrading note Closes #1403. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Permitted sorting can invoke unlisted sort scopes, contradicting the documented authorization invariant.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Adds opt-in trust of permitted Rails strong parameters as Ransack’s authorization boundary.
Changes:
- Adds global and per-search configuration.
- Routes attribute, association, and sorting authorization through permitted parameters.
- Adds tests and migration/security documentation.
| File | Description |
|---|---|
lib/ransack/search.rb |
Detects permitted parameters and configures the context. |
lib/ransack/context.rb |
Selects existence lists for trusted searches. |
lib/ransack/configuration.rb |
Adds the global option. |
spec/ransack/strong_parameters_spec.rb |
Tests the new authorization behavior. |
docs/going-further/other-notes.md |
Documents usage and risks. |
docs/getting-started/upgrading.md |
Adds migration guidance. |
docs/getting-started/configuration.md |
Documents configuration. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| it 'never bypasses ransackable_scopes' do | ||
| expect(Person).not_to receive(:active) | ||
| expect(Person.ransack(permitted(active: true)).result.to_sql).not_to include 'active = 1' |
There was a problem hiding this comment.
Note
Reply from Claude (Claude Code), acting on behalf of @scarroll32.
Right that the sort_by_<name>_<dir> convention is reachable here, but it is reachable in exactly the same way without this setting: Search#add_sort accepts it on respond_to? in every mode, and ransackable_scopes has never gated it. It is reached by name and takes nothing from the request, so it is a different thing from a condition scope with a user-supplied argument. This PR neither opens nor closes it.
Fixed by narrowing the invariant rather than the code: the docs now say "a permitted condition key can never call a class method the model has not listed" and name the sort convention as separate, and a new spec pins that a trusted s: 'reverse_name asc' produces byte-for-byte the same SQL as the plain-Hash search. Gating the convention behind ransackable_scopes would be a behaviour change for every existing user and is out of scope here.
| What is permitted is the condition key, so `title_cont` allows exactly that | ||
| and not `title_eq`. `s` is a string for a single sort and an array for | ||
| several, hence both forms. Use `fetch` rather than `require`, because a request | ||
| with no search is a normal request. |
There was a problem hiding this comment.
Note
Reply from Claude (Claude Code), acting on behalf of @scarroll32.
Agreed, and documented. permit(:s) permits the key and cannot express a list of columns, so with the setting on a permitted s sorts by anything that exists. The Authorization docs now have a paragraph saying exactly that, a controller example that validates the sort value against its own list before passing it on, and the alternative of strong_parameters: false for that search so ransortable_attributes applies. A spec covers the override.
Two Copilot findings on the PR, both about sorting: - `permit(:s)` permits the key, not the column named in its value, so a trusted `s` can sort by anything that exists. The docs now say so and show a controller validating the value against its own list, or passing `strong_parameters: false` so `ransortable_attributes` applies. Spec for the override. - The `sort_by_<name>_<dir>` convention is reached by name in every mode and is not gated by `ransackable_scopes`; it takes nothing from the request. The "scopes never bypass" invariant now says "condition key" and names the convention as separate, and a spec pins that a trusted search produces the same SQL as a plain one for it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@deivid-rodriguez, @gregbell thoughts on moving to this into the 6.0.0 release of Ransack? |
A model that defines ransackable_attributes, ransackable_associations or ransortable_attributes keeps them for every search, permitted or not. A model that defines none lets permitted ActionController::Parameters be the boundary, and still raises for a plain Hash or unpermitted params. ransackable_scopes applies regardless. Drops the strong_parameters configuration and per-call option from the earlier revision: every existing app defines the lists, so "model list wins when defined" makes the upgrade a no-op and leaves nothing for a switch to choose. Makes explicitly_defined? public as ransackable_list_defined? so the context can ask. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>


Will be merged to
mainat the Start of October if there is no comment, and v6.0.0 will be cut at the end of OctoberCloses #1403. Part of #1640.
Why
Ransack discarded the
permitted?flag. The first thingSearch#initializedid wasto_unsafe_h, so strong parameters filtered keys but never authorized anything, and every model needed its own allowlists even when the controller had already said exactly which keys may be searched.What
No configuration. The model decides which layer applies:
ransackable_attributes,ransackable_associationsorransortable_attributeskeeps them. They apply to every search, permitted or not, as in 4.x and 5.x. A key must be permitted by the controller and in the model's list.ActionController::Parametersmay use every permitted key that names a real column, ransacker, alias or association.searchable_attributes/sortable_attributes(the form helpers) list the same.ransackable_scopesis always consulted, whatever the model defines and whatever was permitted. Thesort_by_<name>_<dir>sort-scope convention is separate: reached by name, takes nothing from the request, neither opened nor closed here.ransack!, as before.author_name_conton a listlessArticlestill consultsAuthor's lists ifAuthorhas them.Every existing app already defines the lists, because 4.0 raises otherwise, so the upgrade is a no-op. The only new behaviour is that a model without lists now works through a permitting controller instead of raising. Giving a model a list is the override; there is no mode and no per-call option.
Permitting
spermits the key, not the columns named in its value, so on a listless model a permittedssorts by anything that exists. The docs say so, show a controller validating the value, and point atransortable_attributeswhere sorting must stay restricted.permit(q: {})permits everything underqand Ransack cannot tell it apart from an explicit list, so on a listless model that action is a full bypass. The docs say so in a warning box, a spec pins it, and the same spec shows a model list still applying to it.The proposal, as decided
The options are on the issue and the decided design is at the top of the issue body. @dukz's point that these are two separate layers led to dropping the
strong_parametersconfig and per-call option from the earlier revision of this PR: with "model list wins when defined" there is nothing left for a switch to choose.How
Search#initializereadspermitted?before the unwrap and stores it onContext#permitted.Ransack::ActiveRecord::Base#ransackable_list_defined?is the existing privateexplicitly_defined?made public: whether the model, or a superclass, defines the given list rather than inheriting Ransack's default.Contextroutes the threeransackable_*?checks and the three form-helper lists through one privateallowlistlookup: the model's list when it defines one, theauthorizable_ransackable_*existence list when it defines none and the params were permitted, and otherwise the model's default, which raises.ransortable_attributescounts as defined if either it orransackable_attributesis.Docs
sin both shapes;fetchnotrequire), how a model list stacks on top, the three invariants, and thepermit(q: {})warning.Verification
strong_parameters_spec.rb(16 examples) usesUnlisted, a model inheriting fromActiveRecord::Basedirectly so it has no lists: any column, sort and association through permitted params; the traversed model's list still applies; existence underransack!; scopes never bypassed (expect(Unlisted).not_to receive(:active)); plain Hash and unpermitted params raise;permit(q: {}). And forPersonand aListedmodel: the list applies to permitted params, to sorting, topermit(q: {}), to an STI subclass, and to the form helpers.🤖 Generated with Claude Code