Skip to main content
Test Double company logo
Services
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say Hello
Test Double logo
Menu
Services
BackGrid of dots icon
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
Cycle icon
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say hello
Developers
Developers
Developers
Rails upgrades

Guide to incremental Rails upgrades for smoother transitions

Join us as we explore a more incremental approach to Rails upgrades that keeps your team moving forward without the stress. Perfect for agile development teams.
Ali Ibrahim
|
May 11, 2021
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

In the past few years, I’ve seen a huge improvement in how teams upgrade their Rails apps. Instead of starting a new branch and seeing where it goes, we can now use tools like Bootboot to do Rails upgrades alongside everday development.

This lets us do Rails upgrades incrementally. Instead of having a pile of changes idling in a branch, we can make small changes that everyone can see.

This is awesome! But it does present some new challenges.

Supporting two versions of Rails all the damn time

When we take a dual boot approach to upgrading, we’re gonna run into situations where the production version of Rails expects one thing but the new version of Rails expects something different. For example, before Rails 5.0 URL and routing helpers accepted any and all params.

>> params = ActionController::Parameters.new(user_id: 23)
>> root_path(params)
✅ "/?user_id=1"

But starting in Rails 5.0, URL and routing helpers will raise ArgumentErrors if they receive unpermitted params.

>> params = ActionController::Parameters.new(user_id: 23)
>> root_path(params)

>> params = ActionController::Parameters.new(user_id: 23)
>> root_path(params)

‼️ ArgumentError: Attempting to generate a URL from non-sanitized request parameters! An attacker can inject malicious data into the generated URL, such as changing the host. Whitelist and sanitize passed parameters to be secure.

We want to deploy code that works against both versions of Rails, so we’d have to start permitting the params we send to these helpers.

>> params = ActionController::Parameters.new(user_id: 23)
>> params.permit(:user_id)
>> root_path(params)
✅ "/?user_id=1"

In an example like this, where it’s clear what the params are, we can safely update the code. However, imagine we come across something like this:

class UserController < ApplicationController
  def create
    # a bunch of business logic
    redirect_to root_path(params.except(:page, :filter, :sort))
  end
end

It’s clear to see what params we don’t care about (:page, :filter, and :sort), but it’s hard to infer what params are important. So what do we do?

Option 1: permit! all the params

We could continue to exclude the same params and permit! all the rest.

redirect_to root_path(params.except(:page, :filter, :sort).permit!)

This will make the ArgumentError go away, but we didn’t really address the issue Rails is warning us about. By permitting all the params, malicious users could do things like change the host so users get redirected away from the app. Not good. 😳

Option 2: permit the params that matter

Instead of excluding params, we should really permit the ones we care about. But we currently don’t have enough information to say what those params are.

There are lots of ways we could get this information. Maybe we have good test coverage, and we can find the params there. Or maybe we can set up some logging to see what params come through on production. Or we might know the developer who wrote this code, and we can ask them for guidance.

If we’re working on an upgrade where we only see this problem a handful of times, we can take the time to do the due diligence to make sure the right params get permitted. But if we’re working on a large upgrade where this is a widespread problem, making these changes is going to require a bigger investment which could ultimately delay the upgrade. Now what?

Option 3: set up systems for incremental change

With the first two options, we either ignore the problem or stop everything to fix it. Instead, we can take a hybrid approach that unblocks the upgrade while also organizing future work that can be done outside the upgrade cycle.

First, we can introduce a new helper method called unsafe_params.

module UnsafeParams
  extend ActiveSupport::Concern

  def unsafe_params
    params.dup.permit!
  end

  included do
    helper_method :unsafe_params
  end
end

Then, we can use unsafe_params wherever we’re running into ArgumentErrors for unsanitized params.

redirect_to root_path(unsafe_params.except(:page, :filter, :sort))

This might look a lot like just permitting all the params, and it’s not far off! The difference is how this will help us in the future. Instead of having to comb through the codebase for every permit! call and deciding if it could be changed, we’ll be able to narrow our focus to searching for unsafe_params. With unsafe_params we’ve codified a change in Rails we should adopt, and now we can do it on our own timeline.

unsafe_params corrals the existing problem, but it doesn’t prevent more of the same bad code from sneaking in behind us. With development still happening against older versions of Rails where this isn’t a problem, folks can keep sending whatever params they want to URL and routing helpers. How can we get people to change their habits and adopt the new Rails behavior?

Option 3b: push people towards change

We can help everyone on our team adjust to this new behavior by bringing the change to them. We’ll do this by backporting the Rails 5.0 behavior with a few small changes.

if App.rails_4_2?
  module UrlForInsecureUrlParametersPatch
    def url_for(options = nil)
      if options.is_a?(ActionController::Parameters) && !options.permitted?
        report_insecure_url_parameters
      end

      super
    end

    private

    def report_insecure_url_parameters
      message = <<-MSG.squish
        Attempting to generate a URL from non-sanitized request parameters!

        An attacker can inject malicious data into the generated URL, such as
        changing the host. Whitelist and sanitize passed parameters to be secure.
      MSG

      if Rails.env.development? || Rails.env.test?
        raise ArgumentError, message
      else
        Logger.warn(
          error: "ArgumentError",
          cause: "insecure_url_parameters",
          message: message,
          backtrace: caller
        )
      end
    end
  end

  ActionController::Base.prepend UrlForInsecureUrlParametersPatch
end

First, the backport is wrapped in an if App.rails_4_2? clause, because we only want to use this backport in the older version of Rails.

Then, if we receive unpermitted params we’ll report it. Unlike the Rails 5.0 implementation, we’ll only raise errors in dev and test.

if Rails.env.development? || Rails.env.test?
  raise ArgumentError, message
else
  Logger.warn(
    error: "ArgumentError",
    cause: "insecure_url_parameters",
    message: message,
    backtrace: caller
  )
end

So if a developer leans on their old habits and sends unpermitted params, they’ll see the same error they would see if they were developing against Rails 5.0. The easiest path forward is to then permit the params! 🥳

We’ll only log this error in production because we don’t want to immediately break application behavior. Logging these errors before switching to Rails 5.0 on production will also help uncover any gaps that we might’ve missed in the test suite.

Pragmatism FTW

Rails upgrades can be a slog. I’ve worked on Rails upgrades with hundreds of test failures, where it felt like we could never keep up with all the changes. So when we run into problems like the one I described in this blog post, it can be tempting to reach for the first solution that fixes the problem in front of us.

However, if we take a few minutes to step away from the problem, we might come up with creative solutions to keep everyone moving forward.

Related Insights

🔗
Zero downtime Rails upgrades
🔗
Managing Ruby hashes in Rails upgrades
🔗
Why it’s worth outsourcing your Rails upgrades

Explore our insights

See all insights
Developers
Developers
Developers
LLMallard: the low-key AI chat bot you secretly need

The most perfect dev workflow for taking advantage of deeply powerful AI tooling that’s hyper efficient on token usage with minimal API calls—and the perfect pair programming partner.

by
Daniel Huss
Leadership
Leadership
Leadership
Why we coach the system, not just the team

Slow delivery isn’t usually about your people—it’s about your system. Shifting focus to incremental improvements in the system helps change not just processes but behaviors for lasting change.

by
Doc Norton
Developers
Developers
Developers
Developer QA checklist for feature releases

Quality Assurance is a mindset integrated throughout development to catch issues early, build user trust, and reduce maintenance costs. These recommended procedures for dev teams without dedicated QA roles establish collective responsibility for ensuring feature stability, functionality, and usability before release.

by
Lee Quarella
Letter art spelling out NEAT

Join the conversation

Technology is a means to an end: answers to very human questions. That’s why we created a community for developers and product managers.

Explore the community
Test Double Executive Leadership Team

Learn about our team

Like what we have to say about building great software and great teams?

Get to know us
No items found.
Test Double company logo
Improving the way the world builds software.
What we do
Services OverviewSoftware DeliveryProduct ManagementLegacy ModernizationDevOpsUpgrade RailsTechnical RecruitmentTechnical Assessments
Who WE ARE
About UsCulture & CareersGreat CausesEDIOur TeamContact UsNews & AwardsN.E.A.T.
Resources
Case StudiesAll InsightsLeadership InsightsDeveloper InsightsProduct InsightsPairing & Office Hours
NEWSLETTER
Sign up hear about our latest innovations.
Your email has been added!
Oops! Something went wrong while submitting the form.
Standard Ruby badge
614.349.4279hello@testdouble.com
Privacy Policy
© 2020 Test Double. All Rights Reserved.