Do you know those things that are simultaneously incredibly important to get right but incredibly easy to get wrong? That makes for an explosive combination. One such thing happens to be one of the hardest areas in software development: security. Security is hard no matter the language or platform. Today, we’re here to talk specifically about security best practices in Ruby.
This post is pretty straightforward: it’s a good old list of 10 of the most important recommendations for ensuring your Ruby apps are as safe as they can possibly be. By the end of the post, you’ll know more about some of the most important security challenges you must be aware of when coding a Ruby app. You’ll also know some basic but effective practices you can leverage to counter those challenges and make your application more secure.
Before wrapping up, we’ll offer a quick summary of the post, along with a link for a special free resource you can download to learn more about Ruby security.
Let’s dig in.
Ruby vs Ruby on Rails
Before we get to the promised list of 10 Ruby security tips, let’s clarify a potential misconception. This post is about Ruby, the programming language, not Ruby on Rails, the web framework. It’s important to make this distinction since most people would just assume we’re talking about Rails.
Though of course Ruby security problems definitely impact the framework, the best practices in this article are mostly about the language itself, even though some of them might relate to the framework. Importantly, the framework itself does not protect you against all of the following. 70% of Ruby on Rails exploits over the past few years were SQL injections, for example.
Ruby security best practices: 10 for your fun and profit
With that necessary disclaimer out of the way, let’s get to what matters: our list of the 10 best practices you should adopt right now.
1. Avoid unsafe data serialization
There are methods that can be used in data serialization that you should avoid. Those methods include JSON.load
, Marshal.load
, and YAML.load
. When used with user data, these methods might allow malicious users to execute code through the use of malformed JSON or YAML.
Instead of the methods above, default to using methods like JSON.parse
or Pysch.safe_load
.
2. Don’t roll your own cryptography solution
This tip is an easy one, and I hope it makes immediate sense for you: don’t implement your own crypto. Before you dismiss this as just another installment of “don’t reinvent the wheel” advice, read on. There’s more to it than that old—yet sound—advice.
Many developers, teams, and even companies suffer from the infamous not invented here syndrome. Most of the time, this stance is, if not harmless, incapable of causing serious damage beyond the inevitable opportunity cost.
When it comes to security, though, things are different. The price you pay for being wrong is too high, so just don’t risk being wrong. You do that by not creating your own cryptography solution. Instead, research and learn what the state of the art is, and use that instead.
3. Validate data you don’t control
Don’t trust any data that comes from outside of the application. Perform validations on every bit of data submitted by users. If you want your app to be safe, assume that no data from the outside world is trustworthy.
Given Ruby’s dynamic nature, it quickly becomes very intricate to track a piece of data across the application. One way to combat that is to validate data early, as close to the “edge” of the application as possible, to enforce a “safe core” design. Another strategy is to distinguish between unvalidated and validated data via types and signatures, using Sorbet or RBS to enforce typing.
By doing so, you go a long way towards preventing common security exploits, such as SQL injections, or other types of attacks that exploit database vulnerabilities.
4. Don’t commit secrets to the code
Never commit secrets to your code. Secrets here mean things like API keys, passwords, encryption keys, SSH keys, and so on. Maintaining your secrets outside your application code also makes it easier for you to quickly alternate between different environments.
There are different approaches you can use to manage your secrets, but the most common ones are certainly using configuration files to keep secrets from entering your code.
5. Use a pre-commit hook
Every time you commit code to a public repository, you should verify and ensure no sensitive data is being included. Mistakes happen, though, so it’s advisable that you use automation to give yourself an extra layer of confidence.
One way in which you can do that is by using a pre-commit hook. Hooks are scripts you can add to a Git repository. They’re triggered by specific actions. In the case of pre-commit hooks, as the name implies, they are executed right before a commit is added to the repository. You can use hooks to prevent security leaks from happening and then safely abort the commit if a problem is found.
6. Run security linters on your code
Linters are tools that analyze source code looking for signs of problems. They are very popular tools for keeping code quality high. They’re especially popular among dynamic languages such as JavaScript, Python, and—you’ve guessed it—Ruby.
Security linters are also a thing. The idea behind their use is the same as general-purpose linters: they analyze source code searching for red flags. You can add security linters to your CI/CD pipeline to prevent insecure code from reaching production. Security linters lack the context of your application, so they won’t catch everything, but they can catch code-level issues.
RuboCop is a well-known, wide scope linter but it also includes some static security checks as well, and even the syntax and logic checks can hint at subtly faulty code that could result in bypasses.
7. Keep dependencies up to date
It’s virtually impossible to build a nontrivial piece of software without using third-party code.
Adding code written by other people to your projects is a double-edged sword: while on one hand, it frees you to do more interesting and valuable work, it also opens you to security vulnerabilities. The good news is that most of the third-party tools you’ll use will be open-source. There are so many people watching those projects, and security vulnerabilities are typically found and patched quickly if you’ve chosen good tools.
However, you also have to do your part, which is the easiest part, to be honest: you have to make sure your dependencies are kept up to date. It doesn’t matter if the third-party tool is patched if you don’t update it on your end. Thanks to increasing use of semantic versioning by gem authors and RubyGems’s ~>
operator, routinely updating to a non-API-breaking version can be as simple as a bundle update
. The bundle-audit
tool also helps in alerting about known security issues in app dependencies.
8. Log all the things
Logs are powerful allies when it comes to troubleshooting a system in production. They can help teams detect and fix many types of problems. Security breaches are certainly not an exception to that. Leverage logging and come up with an efficient log management strategy.
Centralize all of your logs to a safe place, where you can analyze them. Make sure you don’t include personally identifiable information, like your name or data about your finances, in your log entries, since doing so goes against the GDPR.
9. Don’t roll your own session management
Many security best practices take the form of “don’t implement your own x.” Even though the opportunity cost is definitely a factor that must be taken into account, the main reason for those pieces of advice is to safeguard the security of your application.
Just like with crypto, authentication security is something that’s easy to get wrong. It’s easy to overlook corner cases that result in vulnerable session management code, which will put your application in danger. Instead of rolling your own authentication solution, it’s better to leverage one of the existing solutions.
10. Use a secure code review checklist
Your team or organization should enforce security reviews for every pull request. It should do that by creating a universal security checklist that reviewers can use to evaluate every attempt to merge code to the mainline.
While it’s possible to automate part of these checks with CI-ran tools, some still need to be reviewed by humans. Simple reminders such as adding checkboxes in PR templates can go a long way towards long-term review consistency.
Conclusion
Digital security is a serious business. Having faulty security not only exposes your applications to attacks. It also harms your company’s reputation. Going against regulations such as the GDPR can cause you serious financial and legal trouble, or even put you out of business. But now that we’ve covered Ruby security and measures you can take to achieve it, you’re that much closer to having good security habits.
The list of best practices we’ve shared is just the tip of the iceberg, though: it’s far from being exhaustive. If you want to learn more about Ruby security tips, you can download the Ruby Security Handbook by Sqreen. This is a free ebook that includes the tips shared in today’s post, and many more. After reading the book, you’ll be more prepared to tackle the security challenges plaguing your Ruby app.
Thanks for reading, and until next time.
This post was written by Carlos Schults. Carlos is a .NET software developer with experience in both desktop and web development, and he’s now trying his hand at mobile. He has a passion for writing clean and concise code, and he’s interested in practices that help you improve app health, such as code review, automated testing, and continuous build.