Auditing a Rails app you just inherited
By Rob Bazinet
· 6 min read
So you’ve been handed a Rails app. Maybe you’re the new CTO. Maybe you’re the agency that just took the contract. Maybe you’re the engineer the founder hired after the last engineer left. Either way, the prompt is the same: what’s in here, and how bad is it.
This is the order I run through it. It’s not the only order; it’s the one that gets you the most information in the least amount of time. Roughly half a day end-to-end, longer if the answer is “actually quite bad.”
Step 0: don’t read the code first
You will be tempted. Don’t. Reading code without context tells you nothing — you’ll spend two hours in app/models/user.rb deciding whether before_save :normalize_email is good or weird, and at the end of it you’ll know less than if you’d spent those two hours running tools.
The audit is mechanical. Run the tools first. Read the code when the tools have told you where to look.
The Gemfile.lock is the most useful file in the repo
Open Gemfile.lock. Look at three things:
- The Ruby version. If it’s pinned below 3.0, you have a maintenance problem. Below 2.7, you have an actual security problem (no upstream patches). Pre-2.6, you have an emergency.
- The Rails version. Same calculus — Rails LTS is paid; Rails proper supports the current and one previous major. If the app is on Rails 5.x, you’re going to have a conversation about that no matter what else you find.
- The shape of the dependency tree. Are there 200 gems or 80? Is
rails-controller-testinglisted because they downgraded? Is there a fork URL pointing at a personal GitHub? Are there gems that were last released in 2018?
You learn more from Gemfile.lock in three minutes than from any other file in the repo. The team that built this app made decisions that show up here, plain as day.
Run bundler-audit. Then run it again with –update
bundle exec bundler-audit check --update
If you don’t have it, install it (gem install bundler-audit) — it’s not a Gemfile dependency for a reason: you want it to come from outside the app’s bundle.
If the output is empty, that’s information. If it’s not empty, that’s a list. Read each advisory. Some of them won’t apply to your app at all (you don’t use that part of the gem, or you don’t expose the vulnerable endpoint, or the version range doesn’t match). Some will. The job is to triage them, not to update every single thing the first day. I wrote about this triage process separately — it’s the trickiest part of the audit.
If the app has a yarn.lock, package-lock.json, or pnpm-lock.yaml, run the npm advisory check against it too. JavaScript dependency CVEs are usually less load-bearing for a Rails app than gem CVEs, but they’re not zero — and a CVE in your build toolchain is a CVE in your build.
Brakeman is worth running. It’s not worth fixing everything it finds.
gem install brakeman
brakeman -A
Brakeman will produce a list. Read it. Pay attention to:
- SQL injection warnings. These are almost always real.
- Mass assignment warnings. Usually real, usually fixable in one line.
- Open redirect warnings. Sometimes real, sometimes false positives because of how Brakeman parses redirect targets.
Ignore on first pass:
- Cross-site scripting warnings on raw or html_safe in places that you can see are content the team controls (templates, seed data). Note them, don’t fix them on day one.
- “Weak hash” warnings on md5/sha1 unless they’re being used for security (auth tokens, signed cookies). MD5 on a cache key is fine.
The goal is a list of three to ten items you’d consider real findings, not the full Brakeman report.
Read config/environments/production.rb
This is the second most useful file in the repo, after Gemfile.lock. You’re looking for:
config.force_ssl = true— should be on.config.assume_ssl = true— should be on if you’re behind a TLS-terminating proxy (and you are).config.log_level—:infois normal,:debugin production is a leak.- Any
secret_key_basehardcoded in source. (Rare these days, but I’ve seen it.) - Whether there’s a CSP setup, error tracking, rack-attack, or anything else you’d expect a production app to have.
A “clean” production.rb doesn’t mean the app is secure. A messy one does mean someone made decisions you should ask about.
Are there tests, and is anyone running them
Open test/ or spec/. Count the files. Look at coverage in .simplecov, or run bundle exec rake test:coverage if it’s wired up, or just open one of the bigger model files and check whether its tests exist.
Then check CI — .github/workflows/, .circleci/, .gitlab-ci.yml, whatever the team is using. Is the test job actually running? Is it passing on main? When did it last fail? GitHub Actions has a “history” view that tells you exactly this.
If there are tests and they pass, you have leverage to make changes. If there are no tests, or the tests are broken and nobody noticed, you have a different problem — every change you make is going to be more expensive than it should be, and the Rails upgrade conversation got harder.
Look at the database
bundle exec rails db:migrate:status
If there are pending migrations on the trunk branch, ask about it before you do anything else. Pending migrations on main usually mean someone shipped the code but not the schema, which is a fun bug to find.
Open db/schema.rb (or structure.sql). Look for:
- Tables without an explicit primary key.
- Columns with
null: truethat probably shouldn’t be (user_id, foreign keys generally,created_at). - Foreign-key columns that don’t have indexes. This is the single most common database problem I find. Find a column named
*_idthat isn’tiditself, and check whether there’s a corresponding index. Usually about half of them aren’t indexed.
Don’t fix any of this yet. Just note it.
Deploy and observability
How does this app get to production? Look for a Dockerfile, a config/deploy.rb, a kamal-deploy.yml, a render.yaml, a fly.toml, a Procfile. The presence and shape of these tells you what platform the team chose and how rough the deploy story is.
Then look for monitoring:
- Is there an error tracker in the Gemfile? (Sentry, Honeybadger, Rollbar, Bugsnag, etc.)
- Is there an APM? (Skylight, Scout, New Relic.)
- Where do logs go in production?
STDOUT? A file? A vendor? - Is there an uptime check pinging the deployed app? At what frequency?
If the answers are “none, none, the disk, no” — that’s not unusual, and that’s the next thing after the security cut.
Write it up
I write the audit as one page, four sections: Security, Tests & quality, Infrastructure, Database & code health. Each section gets the findings, sorted hardest-first, with a one-line plan: fix now, plan for next sprint, document and accept, won’t fix.
That’s the audit. It’s not magic — it’s a list of things to check and a habit of checking them in the same order. Which is also exactly why I built RailsHealth: the work is repeatable, the report is the same shape every time, and once you’ve done it ten times by hand you start to think it should be a program.
If you want this run automatically against a repo and turned into a shareable report, that’s literally what the product is.