a dangerous exercise. There’s a good chance I’ve gotten at least one or more facts wrong in this talk despite taking a good deal of time to research the topic from various angles.
attributes • Handling sessions securely • Cookies • Handling profile pictures • Links to user’s own page • File uploading • Protecting user actions • Protecting against password crackers • Protecting your frontend • + 5 more general tips
SQL TYPECASTING MATCHES WRONG RECORDS '{"user":{"token":0,"pass":"hacked","pass_confirm":"hacked"}}' '{"user":{"token":[0],"pass":"hacked","pass_confirm":"hacked"}}' def reset user = User.find_by_token(params[:user][:token]) if user.present? user.reset_password(params[:user][:pass]) end end
SQL TYPECASTING MATCHES WRONG RECORDS “1aC” is casted to 0 (invalid integer) 0=0 retrieves all records mysql> SELECT reset_token FROM users WHERE reset_token=0; +-------------+ | reset_token | +-------------+ | 1aC | +-------------+ 1 row in set (0.00 sec)
SQL TYPECASTING MATCHES WRONG RECORDS “1aC” is casted to 0 (invalid integer) 0=0 retrieves all records mysql> SELECT reset_token FROM users WHERE 0=0; +-------------+ | reset_token | +-------------+ | 1aC | +-------------+ 1 row in set (0.00 sec)
NOT ESCAPED Using params directly on SQL queries – huge mistake. Hardly a problem on most Rails apps thanks to sane defaults. User.where("name = '#{params[:name]}'")
NOT ESCAPED Using params directly on SQL queries – huge mistake. Hardly a problem on most Rails apps thanks to sane defaults. User.where("name = '#{params[:name]}'") params[:name] = “' OR 1 --“ SELECT * FROM users WHERE name = '' OR 1 –'
NOT ESCAPED Using params directly on SQL queries – huge mistake. Hardly a problem on most Rails apps thanks to sane defaults. User.where("name = '#{params[:name]}'") params[:name] = “'; DELETE FROM employees --“ SELECT * FROM users WHERE name = ''; DELETE FROM employees ' –
– DECODING A SESSION COOKIE In Rails 2 and 3, decoding a session cookie is this simple: Marshal.load( Base64.decode64( URI.decode( cookie.split('--').first )))
– DECODING A SESSION COOKIE In Rails 2 and 3, decoding a session cookie is this simple: Marshal.load( Base64.decode64( URI.decode( cookie.split('--').first )))
– DECODING A SESSION COOKIE In Rails 4 it's a bit harder, 1000 iterations of PBKDF2 with a salt: – Did you create secret_key_base on config/secrets.yml? – If not, your app supports legacy Rails 3 cookies.
– DECODING A SESSION COOKIE With session data in our hands, we can alter it trivially. Not really, because of the session digest. You can only read it. So don’t store anything other than the session ID there!!!
DON'T Whitelist params[:doc_name] # sanitize params[:doc_name] send_file “docs/#{params[:doc_name]}.pdf” The user running your app should have minimal permissions!
DON'T Whitelist params[:doc_name] # sanitize params[:doc_name] send_file “docs/#{params[:doc_name]}.pdf” The user running your app should have minimal permissions! I keep a headcount of people who run their apps as root.
IS HTML_SAFE Users can set a blog URL to be displayed on their profiles <%= link_to "Personal blog", user.blog_url %> equivalent to <%= link_to "Personal blog", user.blog_url.html_safe %>
IS HTML_SAFE Users can set a blog URL to be displayed on their profiles <%= link_to "Personal blog", user.blog_url %> Nothing stops an user from setting blog_url to "javascript:alert('hacked!')"
THE PROTOCOL Run URI.parse on user.blog_url and check the protocol validate :blog_url, :format => { :with => HTTP_REGEX } <%= link_to "Personal blog", user.blog_url %>
Ruby matches regexes by default on multi-line mode. If you're trying to validate, a user name, or an URL: validates :blog_url, :format => { :with => /^http(s)*\S+$/i }
Ruby matches regexes by default on multi-line mode. If you're trying to validate, a user name, or an URL: validates :blog_url, :format => { :with => /^http(s)*\S+$/i } $ in the above regex would stop at \n
Imagine you have an API that depends on other API Example: power operations on servers api :PUT, "/hosts/:id/power" @host.power.send(params[:power_action])
FORCE LOGIN ATTEMPTS Your application gets hundreds of requests per second trying to brute force a password for a certain user. PROBLEM – DOS An attacker wants to throw down your site after you published something you should have not. Or just for kicks.
RACK::ATTACK On config/initializers/rack-attack.rb # Throttle high volumes of requests by IP address throttle('req/ip', limit: 20, period: 20.seconds) do |req| req.ip unless req.path.starts_with?('/assets') end
SERVED OVER JSON When here the JavaScript renderer emits e.g. JQuery fragments like: $("#messages").html(.. # load inbox from JSON Attackers can just <script src="http://example/inbox/messages?format=js"> And do whatever they want with the inbox messages
SERVED OVER JSON Phishing with JSONP leaks easy as pie → # Hi @poorSoul, have you seen what they say about you here? Link # * @poorSoul clicks * # Thanks for your inbox!
– CROSS SITE SCRIPTING Any request that modifies a resource should be triggered GET is loaded automatically. <img src="http://example.com/projects/1/destroy“ />
– CROSS SITE SCRIPTING Any request that modifies a resource should be triggered GET is loaded automatically. <img src="http://example.com/projects/1/destroy“ /> <img src="http://example.com/users/admin/edit?password=lol“ />
– USE PUT, PATCH, POST If any of these routes use PUT, PATCH or POST nothing happens <img src="http://example.com/projects/destroy“/> <img src="http://example.com/users/admin“/> <img src="http://example.com/light/392/on“/>
UNVERIFIED REQUESTS class ApplicationController protect_from_forgery :with => :exception rescue_from ActionController::InvalidAuthenticityToken do |exception| sign_out_user end
I'd love to know the use case for plain render. Just use render :plain => params[:name] SOLUTION – AVOID PLAIN RENDER I'd love to know the use case for plain render. Just use render :plain => params[:name]
When TLS is unavailable, browser downgrades to SSLv3 96.9% of top 1M websites are vulnerable Man-in-the-middle can read HTTPS requests! PROBLEM – TLS DOWNGRADE
When TLS is unavailable, browser downgrades to SSLv3 96.9% of top 1M websites are vulnerable Man-in-the-middle can read HTTPS requests! PROBLEM – TLS DOWNGRADE Client and server support TLSv1.2. Active man-in-the-middle forces a downgrade. TLSv1.1 TLSv1.0 SSLv3 → → → POODLE
VERSION IS EXPOSED The default server header in Webrick exposes you – Server: WEBrick/1.3.1 (Ruby/1.9.3/2012-04-20) Other servers suffer from the same issue – 'Server' in Apache, Thin and Webrick – 'X-Powered-By' on Nginx
a dangerous exercise. There’s a good chance I’ve gotten at least one or more facts wrong in this talk despite taking a good deal of time to research the topic from various angles.