Insecure Deserialization in Rails with Jwt Tokens
Insecure Deserialization in Rails with Jwt Tokens
Insecure deserialization occurs when an application processes untrusted serialized data in a way that can alter program logic or trigger unintended behavior. In Ruby on Rails, this risk can intersect with JSON Web Tokens (JWT) when the token payload or custom claims are deserialized without strict type constraints or validation. JWTs are typically encoded as JSON and cryptographically signed; however, the decoded payload is often passed directly into application logic that performs deserialization (e.g., JSON.parse, YAML.safe_load, or ActiveModel::Serializer). If the application trusts the decoded payload and uses it to instantiate objects or evaluate types without sanitization, attackers can inject malicious serialized data or manipulate claims to escalate privileges or bypass authorization.
Consider a scenario where a Rails API decodes a JWT and uses a claim to determine user roles. If the claim is deserialized in a permissive manner, an attacker could embed a serialized object (e.g., using YAML or complex Ruby objects) within the payload if the application later performs unsafe deserialization. Known attack patterns include CVE-2015-3227 in older Ruby YAML parsing, where malicious payloads lead to remote code execution via serialized objects. Even when using JSON, Rails parameter parsing can inadvertently enable injection if developers call methods like ActiveSupport::JSON.decode and then pass the result into methods that instantiate models or modify behavior based on type. The risk is amplified when JWTs are used for identity propagation but the application does not validate the structure or enforce strict schema checks on claims.
To illustrate, an insecure flow might decode a JWT and directly deserialize a field without constraints:
decoded = JWT.decode(token, Rails.application.secrets.secret_key_base).first
user_data = YAML.safe_load(decoded['extra_claims']) # Unsafe if attacker controls payload
In this example, if an attacker can influence the extra_claims field within the JWT (e.g., via a compromised client or a misconfigured issuer), they could supply a malicious serialized payload that executes code when YAML.safe_load is improperly configured. Even when using JSON, permissive parsing and subsequent object instantiation can lead to Insecure Deserialization. MiddleBrick scans detect such risky patterns by correlating JWT usage with deserialization calls and flagging missing schema enforcement or unsafe parsing methods.
Jwt Tokens-Specific Remediation in Rails
Remediation focuses on strict validation of JWT claims, avoiding unsafe deserialization, and enforcing schema checks. Do not rely on JWT payloads for security decisions without verifying structure and types. Use strong parameter validation and prefer whitelisting over parsing arbitrary data.
- Validate and limit claims explicitly: Use a schema validator (e.g., dry-validation or ActiveModel::Errors) to ensure claims match expected formats.
- Avoid unsafe deserialization methods: Replace YAML.safe_load with JSON parsing when possible, and never deserialize user-influenced data without strict type constraints.
- Enforce issuer and audience checks: Verify
issandaudclaims to prevent token misuse across services.
Secure JWT decoding and claim validation example
Use a dedicated JWT library with strong verification and claim validation. Do not pass raw decoded claims into unsafe deserialization routines.
require 'openssl'
require 'jwt'
class DecodeTokenService
def self.call(token)
key = Rails.application.secrets.secret_key_base
# Verify signature and restrict algorithms
decoded = JWT.decode(token, key, true, { algorithm: 'HS256' })
payload = decoded.first
# Validate required claims with strict types
raise 'Invalid issuer' unless payload['iss'] == 'trusted-service'
raise 'Missing subject' unless payload['sub'].is_a?(String) && !payload['sub'].empty?
# Use strong parameters for role handling instead of raw deserialization
{ sub: payload['sub'], roles: Array(payload['roles']) }
rescue JWT::DecodeError, JWT::VerificationError => e
Rails.logger.error("JWT decode failed: #{e.message}")
nil
end
end
Secure role handling without unsafe deserialization
If you need to store structured data in claims, keep it as simple JSON strings and parse with strict validation instead of YAML or object serialization.
class UserPolicy
def initialize(claims)
@claims = claims
end
def admin?
roles = @claims['roles']
# roles should be an array of strings validated server-side
Array(roles).include?('admin')
end
end
# Usage after safe decoding
claims = DecodeTokenService.call(request.headers['Authorization'].to_s.sub(/^Bearer /, ''))
if claims
policy = UserPolicy.new(claims)
head :forbidden unless policy.admin?
end
Parameter sanitization when using ActiveRecord
When claims are used to query or build records, use strong parameters and avoid passing raw deserialized objects to model methods.
def create
permitted = params.permit(:email, :role).tap do |whitelisted|
whitelisted[:role] = Array(params[:role]).select { |r| %w[admin user].include?(r) }.first
end
@user = User.new(permitted)
if @user.save
render :show, status: :created
else
render json: @user.errors, status: :unprocessable_entity
end
end