Ruby on Rails is one of the most loved combinations in tech. It’s a language and framework that’s accessible to people of varying skill sets and experience.
Its maturity and widespread adoption shows with how much the core team and community care about security. Each release improves the framework's hardiness, but there's still so much we can do as developers to protect our applications.
To help choose where to begin, let's look at 10 categories—following OWASP’s latest category prioritizations—where we as Rails developers can have the most impact when securing our applications.
1. Access control
Limiting access and execution controls for specific users and groups is key to the security of your app. The clearest way to manage this is by establishing defined user roles—usually through groups (such as user, admin, etc.), along with individual ownership permission over records. Here are some easy additional steps you can take.
Avoid skipping access logic
When using except logic for disabling a callback, make sure to avoid accidentally skipping important access control logic. For example, applying the skip_before_action on a parent controller along with the necessary access controls can ensure that all descendant controllers also have proper access logic.
You can also prefer to enumerate all actions to avoid omitting one:
Be careful with the session
There are two key things you should avoid using in the session: sensitive data, and user input. The main reason for this is the separation between "safe" and "unsafe" areas. The session should be an area where only data you define exists.
In addition, encrypt any session and cookie data when possible. Rails offers encrypted options for the cookie jar as of Rails 6 to make this easier.
Finally, avoid the default client-based session storage and prefer a database-based session. This can be done in Rails like so:
Lock it all down by default
With the exception of public resources, you can avoid accidental access problems by locking every resource by default. Start with the most restrictive roles, then slowly peel away and reveal access for additional roles where needed.
This not only prevents new roles from accidentally gaining access to unintended resources, but it also forces you to make a conscious choice when granting access.
Cryptography can sometimes feel "set and forget", but this means it's also easy to skip over if you aren't explicitly looking for it. Cryptographic failures result in data loss and data breaches. Additionally, many data privacy laws require additional layers of protection for sensitive and personal data types.
These are a few key encryption practices to remember in your apps.
To start, make sure every connection uses HTTPS, SFTP, or the equivalent secure protocol for the connection type. As users it’s easy to forget that everything auto-redirects behind the scenes, but that isn’t always the case when accessing resources from code.
Use good defaults
Force all traffic to use SSL.
This will handle most cases, and more importantly, will also help to avoid session hijacking.
Use modern encryption algorithms
Did you know that many of the encryption algorithms that were once the gold standard to protect data at rest haven't aged particularly well? OWASP recommends the best ones for your use case, and avoiding MD5, RC4, Blowfish, SHA1, and many others.
Injection is an attacker's ability to use user interaction points—primarily data input or API paths—to trigger actions or gather data. Commonly found security issues include SQL injection and Command injection.
There's really a single approach to preventing injection: separate commands from queries. This prevents any input made by a user from affecting the state of your application or data.
In scenarios where you want user input to control actions within the application, you can use input indirectly. For example, using a case expression:
4. Use secure design choices
Secure design is a board term that could map many of the tips in this document. With that in mind, the intent is to establish processes that can improve how your team—even if it's a team of one—approaches security.
Inject security into the development lifecycle
DevSecOps professionals and tools can provide tremendous value, especially for developers. They can offer patterns, rules, and best practices to prevent future security mistakes.
For example, open-source SAST tools like Bearer can check your code against a predefined set of rules each time your commit code. Approaches like this act like an extra reviewer.
Use common best practices
In addition to using systems and tools, lean on your stack's best practices. Here are a few for Rails:
- Keep unencrypted sensitive data out of cookies and files.
- Keep sensitive data out of GET parameters.
- Be careful what you send to loggers and third parties.
- Enable app-level encryption whenever possible
- Use the right HTTP verb for the occasion—no modifying data with GET.
Rails is reasonably secure-by-default compared to many web frameworks, but there are still instances where you may need to pay extra attention to configuration settings.
Rails has many configuration options available, but—depending on your Rails version—some of the most secure ones need to be toggled on. Some examples include:
- Enabling default_protect_from_forgery to avoid CSRF attacks.
- Enabling force_ssl.
- Enabling secure mailer actions, like ssl: true for action mailer's stmp_settings
6. Vulnerable components
Learning about dependency vulnerabilities can seem like it's easier than it has ever been, with tools like GitHub's dependabot, GitLab's dependency scanner, and bundler-audit available to notify you whenever dependencies receive updates. That said, they aren't detecting vulnerabilities—only reminding you to update versions.
For known vulnerabilities in components, you can monitor the RubySec Advisory Archive.
7. Identification and authentication failures
Don't hardcode secrets
I know. It's an obvious one, but it happens. There's a reason companies exist to detect this problem. Even GitHub committed a private key to a public repository in late March 2023. Instead, use a secrets management tool and retrieve secrets from a secure location at runtime. Don't forget to add appropriate access roles to these secrets too!
Encourage strong passwords
Don't trust your users to make the best choices. Encourage them to use strong passwords by creating a high floor for password strength.
If you're using an authentication solution like devise, you can begin by forcing a minimum password length. Other gems and services should offer similar configuration options.
Remember to verify certificates
Invalid certificates allow attackers to interfere with communication protocols, so it's good to force OpenSSL to use the correct verify mode.
8. Integrity failures
Software development is so much more about integrating with other services and dependencies than ever before. Knowing when to trust and when to be skeptical is a challenge, so it's best to set up clear processes.
Sign when possible
Incorporate signing in all forms when possible.
- Trusted contributors should sign commits, whether on GitHub or GitLab.
- If your applications consume updates from other services, require signed releases.
Don't be an early-adopter of updates
I know we mentioned earlier that keeping dependencies up-to-date is important, but it helps to wait—or at least scrutinize—updates. There's been a growing trend of compromised open-source gems and modules in recent years. Some as forms of protest, but others as malicious means of siphoning sensitive information.
Keep watch for the community's response to new versions, and let minor updates mature if you aren't in immediate need of their fixes.
Avoid deserializing untrusted data
Ruby and Rails have a bit of a history with code execution due to the deserialization of untrusted YAML data. If you're passing data around in a serialized format, make sure to use safe parsing methods. We generally recommend sticking with pure, data-only, formats such as JSON and using the language's parser before loading the data blindly.
9. Logging and monitoring failures
Monitoring and logging offer an excellent too for identifying suspicious or malicious accounts, but it's important to avoid logging sensitive data under the justification of security.
Prefer logging unique identifiers, like UUIDs, rather than usernames, emails, etc. Take it a step further and encrypt any logged values.This goes for internal logging, but is especially important when using a third-party logging or monitoring service.
Server side request forgery (SSRF) happens when user-supplied URLs are consumed without validation, and was in the news recently with Microsoft Azure services, and a Capital One breach. You can prevent this at the application level with a few common approaches:
Sanitize user input
Just as we mentioned in the injection section earlier, sanitize all user input to prevent malicious code injection that could open up unexpected routing.
Only allow certain routes
Some teams are reactive and use deny-style lists for preventing attacks, but it's better to define a set of allowed URLs and match any user-supplied path against that list. This allows you to indirectly use user input, and also prevents attackers from brute-forcing their way through a deny list.
Paired with the allow list, don't allow redirects that use approved URLs as the basis for a malicious URL.
Keep security at the front of mind
This is but a small set of techniques you can use to harden your Rails applications. We know it can be daunting to keep up with all the possible attack vectors, or to even know where to begin. Remember - security is a journey, not a destination 🙂
That's why we open sourced our SAST CLI to discover, filter, and prioritize security risks and vulnerabilities. It emphasizes areas of your code that could lead to sensitive data exposure, and offers advice on how to go about improving your code. Give it a try today.