Skip to content

Commit 7c264af

Browse files
create initial guide
1 parent fa00e37 commit 7c264af

File tree

1 file changed

+296
-2
lines changed

1 file changed

+296
-2
lines changed

README.md

+296-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,296 @@
1-
# rails-security-guide
2-
Ruby on Rails guide for security audits and best practices
1+
# Rails Security Guide
2+
3+
## Overview
4+
Disclaimer: This security guide isn't intended to be exhaustive
5+
6+
Use this guide before each deployment, or, even better, use an automated process.
7+
8+
**Definitions**
9+
- _Never_: Never means never
10+
- _Don't_: Don't unless you have a really really good reason
11+
- _Avoid_: Avoid unless you have a good reason
12+
13+
**Gems**
14+
* [Brakeman](https://github.com/presidentbeef/brakeman) - A static analysis security vulnerability scanner for Ruby on Rails applications
15+
* [Rack::Attack!!](https://github.com/kickstarter/rack-attack) - Rack middleware for blocking & throttling
16+
* [SecureHeaders](https://github.com/github/secure_headers) - Security related headers all in one gem
17+
* [Sanitize](https://github.com/rgrove/sanitize) - An allowlist-based HTML and CSS sanitizer
18+
* [zxcvbn](https://github.com/bitzesty/devise_zxcvbn) - Devise plugin to reject weak passwords using zxcvbn
19+
* [StrongPassword](https://github.com/bdmac/strong_password) - Entropy-based password strength checking for Ruby and Rails
20+
* [Pundit](https://github.com/varvet/pundit) - Minimal authorization through OO design and pure Ruby classes
21+
22+
**TOC**
23+
* [Injections](#injections)
24+
* [Cross-site Scripting (XSS)](#cross-site-scripting)
25+
* [Authentication and Sessions](#authentication-and-sessions)
26+
* [Authorization](#authorization)
27+
* [Cross-Site Request Forgery](#cross-site-request-forgery)
28+
* [Insecure Direct Object Reference or Forceful Browsing](#insecure-direct-object-reference-or-forceful-browsing)
29+
* [Redirects](#redirects)
30+
* [Files](#files)
31+
* [Cross-Origin Resource Sharing](#cross-origin-resource-sharing)
32+
* [Data Leaking and Logging](#data-leaking-and-logging)
33+
* [Misc](#misc)
34+
35+
## Injections
36+
- [ ] Parameterize or serialize user input (including URL query params) before using it
37+
- [ ] Don't pass strings as parameters to Active Records methods. Use arrays or hashes instead
38+
- [ ] Never use user input directly when using the `delete_all` method
39+
- [ ] Never use user input in system commands
40+
- [ ] Avoid system commands
41+
- [ ] Sanitize ALL hand-written SQL [ActiveRecord Sanitization](https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html)
42+
43+
```
44+
# bad
45+
User.find_by("id = '#{params[:user_id]'")
46+
47+
User.delete_all("id = #{params[:user_id]}")
48+
49+
User.where(admin: false).group(params[:group])
50+
User.where("name = '#{params[:name]'")
51+
52+
# good
53+
User.find(id)
54+
User.find_by(id: params[:id])
55+
User.find_by_id(params[:id].to_i) # better
56+
57+
User.where({ name: params[:name] })
58+
User.where(admin: false).group(:name)
59+
User.where("name LIKE ?", "#{params[:search]}%")
60+
User.where("name LIKE ?", User.sanitize_sql_like(params[:search]) + "%")
61+
```
62+
63+
## Cross-site Scripting
64+
By default, when string data is shown in views, it is escaped prior to being sent back to the browser.
65+
66+
- [ ] Never disable `ActiveSupport#escape_html_entities_in_json`
67+
- [ ] Don't use `raw`, `html_safe`, `content_tag`, or `<%==`
68+
- [ ] Prefer Markdown over HTML
69+
- [ ] Validate and sanitize user input for Urls and Html (including classes or attributes)
70+
- [ ] Never create templates in code (use ERB, Slim, Haml, etc)
71+
- [ ] Never use `render inline` or `render text`
72+
- [ ] Never use unquoted variables in HTML attribute
73+
- [ ] Don't use template variables in script blocks
74+
- [ ] Implement [Content Security Policy](https://guides.rubyonrails.org/v7.0/security.html#content-security-policy-header
75+
) or use SecureHeaders gem if below Rails v5.2
76+
77+
```
78+
# bad
79+
config.action_view.escape_html_entities_in_json = false
80+
<%= raw @user.bio %>
81+
<%= @user.bio.html_safe %>
82+
<%= link_to "Personal Website", @user.personal_website %>
83+
84+
<div class=<%= params[:css_class] %></div>
85+
<script>var name = <%= @user.name %>;</script>
86+
render inline: "<div>#{@user.name}</div>"
87+
88+
# good
89+
sanitize(@user.bio, tags: %w(b br em i p strong), attributes: %w())
90+
strip_tags("Strip <i>these</i> tags!") # => Strip these tags!
91+
strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>') # => Ruby on Rails
92+
93+
validates :instagram, url: true, allow_blank: true # link_to("Instagram", @user.instagram)
94+
validates :color, hex_color: true # HexColorValidator # <div style="background-color: <% user.color %>">
95+
```
96+
97+
## Authentication and Sessions
98+
- [ ] Use a database based session store
99+
- [ ] Never put sensitive information in the session
100+
- [ ] Set an expiration for the session (Limit: 30 minutes)
101+
- [ ] Limit "Remember Me" functionality to 2 weeks
102+
- [ ] The same timeline can be used for access & refresh tokens
103+
- [ ] Set all cookies and session store as httponly and secure
104+
- [ ] Revalidate cookie values
105+
- [ ] Never store "state" in the session or a cookie
106+
- [ ] Enforce password complexity (min length, no words, etc)
107+
- [ ] Consider captcha on publicly available forms
108+
- [ ] Consider captcha after several failed login attempts
109+
- [ ] Always confirm user emails
110+
- [ ] Require old password to change password (except for forgot password)
111+
- [ ] Expire password reset tokens after 10 minutes
112+
- [ ] Limit password reset emails within a specified timeframe
113+
- [ ] Consider using two-factor authentication (2FA) (required if storing sensitive data)
114+
- [ ] Don't use "Security Questions"
115+
- [ ] Use generic error messages for failed login attempts (Email or password is invalid)
116+
- [ ] add `before_action :authenticate_user!` to ApplicationController and `skip_before_action :authenticate_user!` to publicly accessible controllers/actions.
117+
118+
```
119+
# bad
120+
Rails.application.config.session_store :my_custom_store, expire_after: 2.years
121+
JWT.encode payload, nil, 'none'
122+
123+
# good
124+
Rails.application.config.session_store :active_record_store, expire_after: 30.minutes, httponly: true, secure: true
125+
cookies[:login] = {value: "user", httponly: true, secure: true}
126+
JWT.encode({ data: 'data', exp: Time.now.to_i + 4 * 3600 }, hmac_secret, 'HS256')
127+
config.force_ssl = true
128+
```
129+
130+
## Authorization
131+
- [ ] NEVER do authorization on the frontend
132+
- [ ] Admin interface should be isolated from the user interface
133+
- [ ] Use 2FA on the admin interface
134+
- [ ] Don't use `accepts_nested_attributes_for` for permissions
135+
- [ ] Prefer policies over querying by association (current_user.posts)
136+
- [ ] Always use policies if using multi-user accounts
137+
138+
```
139+
# bad
140+
@posts = Post.where(user_id: params[:user_id])
141+
@comment = Commend.find_by(id: params[:id])
142+
accepts_nested_attributes_for :permission
143+
144+
# good
145+
@posts = current_user.posts
146+
@posts = policy_scope(Post)
147+
@comment = current_user.comments.find_by(id: params[:id])
148+
authorize @post
149+
```
150+
151+
## Cross-Site Request Forgery
152+
- [ ] If you use cookie-based authentication anywhere, use `protect_from_forgery`
153+
- [ ] If you use token-based authentication, you don't need `protect_from_forgery`
154+
155+
```
156+
# Newer versions of Rails use:
157+
config.action_controller.default_protect_from_forgery
158+
159+
# Implementation
160+
class ApplicationController < ActionController::Base
161+
protect_from_forgery with: :exception
162+
163+
rescue_from ActionController::InvalidAuthenticityToken do |exception|
164+
sign_out_user # destroy the user cookies
165+
end
166+
167+
...(rest of file)...
168+
end
169+
```
170+
171+
## Insecure Direct Object Reference or Forceful Browsing
172+
This is basically guessing ids in the path: `https://example.com/user/10`
173+
174+
- [ ] Use UUIDs, [hashids](https://github.com/peterhellberg/hashids.rb), or a non-guessable id
175+
- [ ] Avoid changing the default primary key (`id`)
176+
- [ ] Policies can help mitigate this as well
177+
- [ ] Don't let a user-supplied params to determine which view to render
178+
- [ ] Don't show the numerical `id` in an API call when using a uuid, hashid, etc
179+
180+
```
181+
Stripe Customer ID = cus_9s6XFG2Qq6Fe7v
182+
183+
# don't do this
184+
def show
185+
render params[:user_supplied_view]
186+
end
187+
```
188+
189+
## Redirects
190+
- [ ] Avoid passing any user-supplied params into `redirect_to`
191+
- [ ] If you must use user-supplied URLs for redirect_to... sanitize or use an allowlist
192+
- [ ] Validate with regex using \A and \z as anchors, _not_ ^ and $
193+
- [ ] If your needs are complex, use [Shopify's redirect_safely gem](https://github.com/shopify/redirect_safely)
194+
195+
```
196+
# bad
197+
redirect_to params[:url]
198+
redirect_to URI.parse(params[:url]).path
199+
redirect_to URI.parse("#{params[:url]}").host
200+
redirect_to "https://yourwebsite.com/" + params[:url]
201+
202+
# ok, but not good
203+
redirect_to "https://instagram.com/" + params[:ig_username]
204+
205+
# good
206+
redirect_to user.redirect_url # sanitize beforehand
207+
redirect_to AllowList.include?(params[:url]) ? params[:url] : '/'
208+
```
209+
210+
## Files
211+
- [ ] Avoid user-generated filenames (e.g ../../passwd), assign random names if possible
212+
- [ ] Only allow alphanumeric, underscores, hyphens, and periods
213+
- [ ] Don't process images or videos on your server
214+
- [ ] Always (re)validate on the backend (file size, media type, name, etc.)
215+
- [ ] Process media files asynchronously
216+
- [ ] Use 3rd party scanners if necessary
217+
- [ ] Prefer cloud storage services such as Amazon S3 to directly handle file uploads and storage
218+
219+
## Cross-Origin Resource Sharing
220+
- [ ] Use [rack-cors gem](https://github.com/cyu/rack-cors)
221+
- [ ] Unless your API is open to anyone, don't set wildcard as an origin.
222+
223+
```
224+
# bad
225+
Rails.application.config.middleware.insert_before 0, Rack::Cors do
226+
allow do
227+
origins'*'
228+
resource '*', headers: :any, methods: :any
229+
end
230+
end
231+
232+
# good
233+
Rails.application.config.middleware.insert_before 0, Rack::Cors do
234+
allow do
235+
origins' http://example.com:80' # regular expressions can be used here
236+
resource '*', headers: :any, methods: [:get, :post]
237+
end
238+
end
239+
240+
Rails.application.config.middleware.insert_before 0, Rack::Cors do
241+
allow do
242+
origins' http://example.com:80'
243+
resource '/orders',
244+
:headers => :any,
245+
:methods => [:post]
246+
resource'/users',
247+
headers: :any,
248+
methods: [:get, :post, :put, :patch, :delete, :options, :head]
249+
end
250+
end
251+
252+
Rails.application.config.middleware.insert_before 0, Rack::Cors do
253+
allow do
254+
if Rails.env.development?
255+
origins 'localhost:3000', 'localhost:3001', 'https://yourwebsite.com'
256+
else
257+
origins' https://yourwebsite.com'
258+
end
259+
260+
resource '*',
261+
headers: :any,
262+
methods: [:get, :post, :put, :patch, :delete, :options, :head]
263+
end
264+
end
265+
```
266+
267+
## Data Leaking and Logging
268+
- [ ] NEVER commit credentials, passwords, or keys
269+
- [ ] Use `config.filter_parameters` for sensitive data (passwords, tokens, etc)
270+
- [ ] Use `config.filter_redirect` for sensitive location you redirect to
271+
- [ ] Don't use 403 Forbidden for authorized errors (it implies the resource exists)
272+
- [ ] Don't include implementation details in view comments
273+
- [ ] Don't write your own encryption
274+
275+
## Misc
276+
- [ ] [Encrypt](https://guides.rubyonrails.org/active_record_encryption.html) sensitive data at the application layer
277+
- [ ] Don't do this in routes `match ':controller(/:action(/:id(.:format)))"`
278+
- [ ] Only use `https` gem sources
279+
- [ ] Use blocks for more than one gem source
280+
- [ ] Never set `config.consider_all_requests_local = true` in production
281+
- [ ] Separate gems by environment
282+
- [ ] Don't use development-related gems (better_errors) in public-facing environments
283+
- [ ] Don't make non-action controller methods public
284+
- [ ] Use `JSON.parse` over `JSON.load`
285+
- [ ] Keep dependencies up-to-date and watch for vulnerabilities
286+
- [ ] Don't store credit card information
287+
- [ ] Avoid user-supplied data in emails to other users
288+
- [ ] Avoid user-created email templates (heavily sanitize or markdown if necessary)
289+
- [ ] Use `_html` for I18n keys with HTML tags
290+
291+
292+
**Additional Resources**
293+
* [Official Rails Security Guide](https://guides.rubyonrails.org/security.html)
294+
* [OWASP: Types of XSS](https://owasp.org/www-community/Types_of_Cross-Site_Scripting)
295+
* [OWAS: Session Management](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html)
296+
* [Rails SQL Injections](https://rails-sqli.org/)

0 commit comments

Comments
 (0)