Nov 13, 2026·9 min read·1 visit
ActionController::Parameters#each_key returns the internal unpermitted HashWithIndifferentAccess object instead of the Parameters wrapper when called with a block. Passing this return value to Active Model update or creation methods bypasses Strong Parameters checks. Upgrade to Rails 8.1.4.1 or 8.0.5.2 to resolve the issue.
CVE-2026-126081 is a mass-assignment vulnerability (CWE-915) in the Action Pack component of Ruby on Rails. Prior to versions 8.1.4.1 and 8.0.5.2, the ActionController::Parameters#each_key method was delegated directly to the internal ActiveSupport::HashWithIndifferentAccess instance holding raw request parameters. When invoked with a block, #each_key returned the underlying unpermitted hash rather than the ActionController::Parameters wrapper instance. Applications that pass the return value of #each_key directly into Active Model mass-assignment methods bypass Strong Parameters enforcement, allowing attackers to modify restricted model attributes.
Ruby on Rails utilizes the Action Pack framework (actionpack gem) to handle incoming HTTP requests and route them to controller actions. Within Action Pack, the Strong Parameters feature (ActionController::Parameters) serves as the primary security mechanism protecting Active Record and Active Model objects against mass-assignment vulnerabilities (CWE-915). It requires developers to explicitly declare which incoming request parameters are permitted for mass assignment before passing them into model persistence or initialization methods.
In affected versions of Ruby on Rails (branches 8.0 prior to 8.0.5.2 and 8.1 prior to 8.1.4.1), an implementation flaw in ActionController::Parameters#each_key allows the Strong Parameters verification check to be bypassed. Instead of returning the ActionController::Parameters wrapper instance after iterating over keys with a block, #each_key returns the internal ActiveSupport::HashWithIndifferentAccess object that stores the raw parameters.
Because Active Model only enforces parameter permission checks on objects that respond to the #permitted? method, passing a raw ActiveSupport::HashWithIndifferentAccess instance to methods such as Model.new, Model#update, or Model#assign_attributes skips authorization enforcement entirely. If an application chains #each_key and passes the result to a model operation, an unauthenticated remote attacker can inject arbitrary attributes into the HTTP request to modify sensitive database fields.
This vulnerability is a regression and incomplete remediation of a previously disclosed flaw, CVE-2020-8164. While CVE-2020-8164 corrected identical return-value leaks in #each, #each_pair, and #each_value, the #each_key method remained directly delegated to the internal parameter hash.
To understand the root cause of CVE-2026-126081, it is necessary to examine the interaction between ActionController::Parameters and ActiveModel::ForbiddenAttributesProtection. When an HTTP request arrives at a Rails controller, the query parameters and request body are parsed into an ActionController::Parameters instance. Internally, this class encapsulates an @parameters instance variable, which is an object of type ActiveSupport::HashWithIndifferentAccess.
When a parameter object is passed to an Active Model mass-assignment method (such as assign_attributes), Active Model invokes sanitize_for_mass_assignment. This internal check inspects whether the incoming argument responds to #permitted?. If the argument responds to #permitted? and the method returns false (the default state for ActionController::Parameters until .permit(...) is called), Rails raises an ActiveModel::ForbiddenAttributesError. However, if the argument does not respond to #permitted?—which is the case for standard Ruby Hash objects and ActiveSupport::HashWithIndifferentAccess—Active Model assumes the hash was constructed internally by the application and permits all attributes within it.
In actionpack/lib/action_controller/metal/strong_parameters.rb, ActionController::Parameters used Active Support's delegate macro to forward several read-only and utility methods directly to the internal @parameters hash:
delegate :keys, :empty?, :exclude?, :include?,
:as_json, :to_s, :each_key, to: :@parametersIn standard Ruby Hash implementations (and HashWithIndifferentAccess), calling #each_key without a block returns an Enumerator. However, calling #each_key with a block yields each key to the block and then returns self (the receiver of the method call). Because #each_key was delegated to @parameters, the receiver of the method call was @parameters rather than the outer ActionController::Parameters instance. Consequently, params.each_key { |k| ... } returned the unpermitted internal ActiveSupport::HashWithIndifferentAccess directly to the caller, stripping the Strong Parameters protection wrapper.
The vulnerability was resolved by Mike Dalessio in commits 8ba746e0a786458c281060823f2ccc5c5f6312ac (for the 8.1 branch) and 2fc8838300133931e19eed3ea6cee76a7e83f19c (for the 8.0 branch). The patch modifies actionpack/lib/action_controller/metal/strong_parameters.rb in two primary ways: it removes :each_key from the delegate statement and defines an explicit each_key wrapper method on ActionController::Parameters.
@@ -248,7 +247,7 @@ class Parameters
# Returns the content of the parameters as a string.
delegate :keys, :empty?, :exclude?, :include?,
- :as_json, :to_s, :each_key, to: :@parameters
+ :as_json, :to_s, to: :@parameters
alias_method :has_key?, :include?
alias_method :key?, :include?
@@ -420,6 +419,15 @@ def each_value(&block)
self
end
+ # Calls block once for each key in the parameters, passing the key. If no block
+ # is given, an enumerator is returned instead.
+ def each_key(&block)
+ return to_enum(:each_key) unless block_given?
+ @parameters.each_key(&block)
+
+ self
+ end
+
# Returns a new array of the values of the parameters.
def values
to_enum(:each_value).to_aIn the patched implementation, ActionController::Parameters#each_key first checks whether a block was provided using block_given?. If no block is present, it returns an Enumerator bound to the Parameters instance via to_enum(:each_key). If a block is provided, it forwards the block to @parameters.each_key(&block) to execute the iteration, but explicitly returns self (the ActionController::Parameters instance) on the final line. This matches the existing secure pattern used by #each_pair and #each_value.
> [!NOTE]
> When evaluating the completeness of the fix against the remaining delegated methods (:keys, :empty?, :exclude?, :include?, :as_json, :to_s), none of the boolean, array, or string methods return a hash structure accepted by mass assignment, with the exception of #as_json.
Crucially, the patch authors left :as_json in the delegate list because its explicit contract is to serialize the object into a primitive Ruby Hash representing JSON data. However, to prevent developer misuse, the patch updated the RDoc documentation for #as_json to explicitly warn that it behaves similarly to #to_unsafe_h:
# Like `#to_unsafe_h`, the hash holds every parameter, permitted or not, and
# mass assignment does not check it. Do not pass it to a model.
#
# params = ActionController::Parameters.new(name: "Bob", admin: true)
# params.as_json # => {"name" => "Bob", "admin" => true}
# User.new(params) # => ActiveModel::ForbiddenAttributesError
# User.new(params.as_json) # => #<User name: "Bob", admin: true>Therefore, while the fix for #each_key is complete and prevents unintended unwrapping during key iteration, security auditors reviewing Rails codebases should remain aware that passing params.as_json to a model constructor remains an unsafe pattern by design.
Exploiting CVE-2026-126081 requires a specific coding pattern within the target Rails application. Rails does not call #each_key internally during standard request processing or parameter permitting. Exploitation is only possible if application code invokes #each_key with a block on an unpermitted ActionController::Parameters object and passes the return value into an Active Model mass-assignment sink.
This pattern occurs when developers chain parameter validation, logging, audit tracking, or key format verification directly prior to updating or creating a record, mistakenly assuming either that #each_key preserves the Parameters wrapper or that Strong Parameters are enforced elsewhere. Consider the following vulnerable controller implementation:
class UsersController < ApplicationController
def update
@user = User.find(params[:id])
# Developer logs incoming attribute keys or validates key formatting
audited_params = params[:user].each_key do |k|
Rails.logger.info("User update requested for attribute: #{k}")
end
# VULNERABLE: audited_params is now an ActiveSupport::HashWithIndifferentAccess.
# Active Model does not raise ForbiddenAttributesError.
if @user.update(audited_params)
render json: @user, status: :ok
else
render json: @user.errors, status: :unprocessable_entity
end
end
endTo exploit this endpoint, an attacker constructs an HTTP PATCH or PUT request containing additional, unauthorized model attributes alongside normal attributes. For example, if the users database table contains an admin boolean column or a role string column, the attacker submits the following JSON payload:
PATCH /users/42 HTTP/1.1
Host: vulnerable-app.example.com
Content-Type: application/json
{
"user": {
"name": "standard_user",
"admin": true,
"role": "superuser"
}
}When the controller executes params[:user].each_key { ... }, the block logs the keys "name", "admin", and "role". The method then returns the underlying HashWithIndifferentAccess containing {"name" => "standard_user", "admin" => true, "role" => "superuser"}. When @user.update(audited_params) executes, Active Model sees a plain hash without a #permitted? method, assigns all three attributes to @user, and commits the changes to the database, granting administrative privileges to the user.
The severity of CVE-2026-126081 is rated as High, with a CVSS v3.1 base score of 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N). The vector reflects that the vulnerability is exploitable over the network (AV:N) with low attack complexity (AC:L), requiring neither privileges (PR:N) nor user interaction (UI:N) when exposed on public-facing endpoints such as user registration or unauthenticated API handlers.
The primary security impact is exclusively focused on Integrity (I:H), with no direct impact to Confidentiality (C:N) or Availability (A:N) in the base CVSS calculation. By bypassing Strong Parameters, an attacker can overwrite any column on the target Active Record model or associated models (if accepts_nested_attributes_for is configured on the model).
In practice, successful exploitation of mass assignment leads to critical secondary impacts depending on the schema of the affected model:
admin, is_staff, role_id) or organization ownership (account_id, tenant_id) on user or session models.balance, paid: true, verified_at, or status: 'approved') on transaction or order models.email, password_digest, or reset_password_token on user records.Current threat intelligence indicates an EPSS score of 0.00625 (50.49th percentile), and the vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog. Because exploitation depends on non-standard application code patterns rather than a default framework configuration, automated mass exploitation across arbitrary Rails targets is unlikely.
The primary remediation for CVE-2026-126081 is to upgrade Ruby on Rails (rails and actionpack gems) to a patched release. The Rails core team has published fixed versions for the supported 8.0 and 8.1 release branches:
Update your application's Gemfile to specify the patched version and run Bundler to update the lockfile:
bundle update rails actionpackFor development and security teams unable to upgrade immediately, or those conducting defense-in-depth code audits, scan the application repository for instances where .each_key or .as_json is called on params objects:
grep -rnE "params(\[[^\]]+\])*\.(each_key|as_json)" app/If any controller or service object iterates over parameter keys using #each_key, refactor the code so that mass assignment strictly consumes the return value of .require(...).permit(...), rather than the return value of the iteration block:
# Secure Pattern: Explicitly permit parameters before iteration or assignment
def user_params
params.require(:user).permit(:name, :email)
end
def update
permitted = user_params
permitted.each_key { |k| Rails.logger.info("Updating key: #{k}") }
@user.update(permitted)
endCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
actionpack rails | >= 8.1, < 8.1.4.1 | 8.1.4.1 |
actionpack rails | < 8.0.5.2 | 8.0.5.2 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-126081 (GHSA-wm3x-v3qw-4jh3) |
| Weakness Class | CWE-915 (Mass Assignment / Improperly Controlled Object Attributes) |
| CVSS v3.1 Score | 7.5 HIGH (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N) |
| EPSS Score | 0.63% (50.49th Percentile) |
| Affected Component | actionpack (ActionController::Parameters#each_key) |
| Security Impact | Strong Parameters Bypass / Unauthorized Attribute Modification |
| Exploit Status | No Known Public Exploits |
| CISA KEV Status | Not Listed |
The application receives input from an upstream component, but it does not restrict or incorrectly restricts the attributes that can be modified in dynamically-determined objects.
A vulnerability classified under CWE-345 (Insufficient Verification of Data Authenticity) exists in the Active Storage component of Ruby on Rails prior to versions 8.1.4.1 and 8.0.5.2. When Google Cloud Storage (GCS) is configured as the storage backend with the cache_control option enabled, Active Storage switches from GCS V2 signing to V4 signing for direct upload URLs. Because the V4 signing implementation only binds the Cache-Control and Host headers, the Content-Type and Content-MD5 headers remain unsigned. An attacker can modify the Content-Type header during a direct upload request to store HTML or SVG files with active JavaScript on the GCS bucket origin, enabling Service Worker registration and interception of subsequent signed URL responses within the bucket path.
A Cross-Site Scripting (XSS) vulnerability exists in Ruby on Rails Action View prior to versions 8.0.5.2 and 8.1.4.1. When applications use the default HTML5 sanitizer (Rails::HTML5::Sanitizer) with Nokogiri versions prior to 1.19.5, angle brackets (< and >) inside HTML attribute values are serialized unescaped. Action View text helpers—specifically highlight and simple_format—perform string-based manipulations on the sanitized HTML rather than operating on the DOM tree. An unescaped '>' character or a double newline ('\n\n') inside an attribute value causes these helpers to misidentify tag or paragraph boundaries, breaking out of the attribute context and emitting inert attribute contents as live HTML elements in the final html_safe output.
CVE-2026-126070 is a Cross-Site Scripting (XSS) vulnerability in the Action View component of Ruby on Rails. Prior to versions 8.0.5.2 and 8.1.4.1, the strip_tags helper returned sanitized output marked as html_safe without encoding single or double quotation marks. When applications render the output of strip_tags inside quoted HTML attributes in ERB templates, attackers can break out of the attribute context and inject arbitrary HTML attributes or JavaScript event handlers.
CVE-2026-126078 is a Cross-Site Scripting (XSS) vulnerability classified under CWE-79 affecting the Action View component of Ruby on Rails in versions prior to 8.0.5.2 and 8.1.4.1. The vulnerability resides in the ActionView::Helpers::TextHelper#highlight method when invoked with a custom :highlighter template that places capture group placeholders inside HTML attribute values. Because matched substrings are substituted without HTML escaping after initial sanitization preserves literal quotation marks, an attacker controlling the input text or target search phrase can break out of the surrounding attribute delimiter and inject arbitrary HTML attributes or JavaScript event handlers.
An information exposure vulnerability in Ruby on Rails Action Pack allows sensitive session cookies to be attached to public, cacheable responses served by Active Storage proxy controllers. When cookie secret rotation or a hybrid cookie serializer migration is enabled, reading a legacy-format cookie triggers an automatic cookie rewrite that ignores the session-skipping directive. If an intermediate shared cache or CDN caches the resulting response along with its Set-Cookie header, subsequent users requesting the same URL receive the leaked session token.
CVE-2026-126072 (GHSA-rwvh-fw6r-pwvp) is a concurrency deadlock vulnerability in the Ruby on Rails Action Pack framework affecting applications that utilize ActionController::Live response streaming. The defect occurs due to improper synchronization within ActionController::Live::Buffer when handling full bounded queues during stream closure and connection abortion. An unauthenticated remote attacker can trigger this condition by stalling TCP socket reads on streaming endpoints—including Active Storage proxy mode—resulting in permanent worker thread exhaustion and application-wide denial of service.