• OWASP nice examples of attack and vulnerability XSS there you can see first two links:

https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet https://www.owasp.org/index.php/DOM_based_XSS_Prevention_Cheat_Sheet Some rails securiry related staff https://github.com/pxlpnk/awesome-ruby-security

  • html image src could be stored:
<IMG SRC=/ onerror="console.log('cao. bolje da eskejpujete sadrzaj');location.assign('http://www.iznajmljivanjeprojektoranovisad.in.rs')"></img>

In rails you can simulate with:

user.name = 'Name "><img src=x onerror=alert("Hi")>'
user.save
# so there is a problem if you have in view
<%= user.name.html_safe %>
<%=raw user.name %>
  • inspect post/patch request in chrome developer tools on Network tab, right click on request and “Copy as cURL”, and update fields to insert your html

  • in rails, copy as curl, for xhr post, I need to add --request PATCH and --data @data.txt and to join all lines with & in data.txt from Copy all Data.

Prevent injection with https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#Output_Encoding_Rules_Summary

  • html entity: & to &amp;, < to &lt;, > to &gt;, quotation mark " to &quot; (&#22; hex &34; dec), apostrophe ' to &x27; (&39; decimal), / to &#x2f; https://www.w3schools.com/html/html_charset.asp
  • html attribute: escape all characters except aphanumeric with html entity &#xHH; and space &#x;
  • url encoding: use percent encoding %XX only for parametar values, not the url of path fragments. Space is replaced with + or %20, " to %22, # to %23, $ to %24, % to %25, & to %26, ' to %27, ( to %28, ) to %29, . to %2E, / to %2F, : to %3A
  • javascript: escape all chards with \uXXXX unicode escaping format
  • css hex encoding

In ruby you can escape query params

  require 'uri'

  enc_uri = URI.escape("http://example.com/?a=\11\15")
  p enc_uri
  # => "http://example.com/?a=%09%0D"

  p URI.unescape(enc_uri)
  # => "http://example.com/?a=\t\r"

Some common escaped characters are: blank, slash URI.escape " /" # %20%2f but escape does not convert / to %2f

DOS attack

  • sudo hping3 -c 10000 -d 120 -S -w 64 -p 21 --flood --rand-source myapp.herokuapp.com
  • you can generate authentication key for http basic authentication with
auth=`echo -n 'username:password' | openssl base64`

Siege

Run 25 concurent users 1 minute with basic http authorization:

siege myapp.herokuapp.com -c25 -t1M -H "Authorization: Basic bmajYXI="

AB

Run 500 requests in 5 threads:

ab -n 500 -c 5 -H 'Authorization:Basic barb2bIjYXI=' http://yourapp.com/
ab -kc 10 -t 10 url # 10 seconds in 10 threads

To check if session cookie is correct add -v 4. Instead of cookie -C you can use -H "Cookie: $MY_COOKIE". Read cookies inside Network panel and Copy as cURL

MY_COOKIE=asd:123
ab -n 1 -C "$MY_COOKIE" -v 4 http://myapp.com/books

MY_COOKIE='Cookie: _asd=ASD; _asd=sds'
ab -n 1 -H "$MY_COOKIE" -v 4 http://myapp.com/books

Rack Attack

You can use rack-attack gem to prevent dos attacks. It will allow all requests from saveflist and exit. Otherwise it will ban all requests from blocklist and exit. Otherwise it will check for throttle. Example configuration

echo 'rack-attack' >> Gemfile
bundle
sed -i config/application.rb -e '/^  end$/i \
    config.middleware.use Rack::Attack'
# config/initializers/rack_attack.rb
module Rack
  class Attack
    # https://github.com/kickstarter/rack-attack/wiki/Example-Configuration
    THROTTLED_BLOCKED_INTERVAL = 1.hour

    # Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}"
    throttle('req/ip', limit: 300, period: 5.minutes) do |req|
      req.ip unless req.path.starts_with?('/assets')
    end

    # Key: "rack::attack:#{Time.now.to_i/:period}:logins/email:#{req.ip}"
    throttle('logins/email', limit: 5, period: 20.seconds) do |req|
      req.params['user'].try(:[], "email") if req.path == '/users/sign_in' &&
                                              req.post?
    end

    blocklist('blocklist_ips 1.2.3.4') do |req|
      Rails.cache.fetch("blocklist_ips #{req.ip}").present?
    end

    OFFICE_IP = '1.2.3.4'
    blocklist('bad_admin_ip') do |req|
      req.path.start_with?('/admin') && req.ip != OFFICE_IP
    end

    blocklist('block bad User agents') do |req|
      req.user_agent.to_s.match(/Nimbostratus|masscan/i)
    end


    safelist('safelist_ips 1.2.3.4') do |req|
      # if req.ip == "127.0.0.1"
      #   true
      # else
      #   Rails.cache.fetch("safelist_ips #{req.ip}").present?
      # end
      Rails.cache.fetch("safelist_ips #{req.ip}").present?
    end

    self.blocklisted_response = lambda do |env|
      ip = find_ip_from_env(env)
      unless Rails.cache.fetch("notification_block #{ip}").present?
        Rails.cache.write("notification_block #{ip}", true, expires_in: 1.hour)
        Notify.exception_with_env(
          "ip_blocklist",
          env: env,
          email_prefix: "just to notify (in this hour) that #{ip} tried again")
      end
      # Using 503 because it may make attacker think that they have successfully
      # DOSed the site. Rack::Attack returns 403 for blocklists by default
      [503, {}, [
        "Your IP address (#{ip}) has been blocked. If you think that this a "\
        "mistake please write to [email protected]"]]
    end

    self.throttled_response = lambda do |env|
      ip = find_ip_from_env(env)
      if ip.present?
        Rails.cache.write("blocklist_ips #{ip}", true,
                          expires_in: THROTTLED_BLOCKED_INTERVAL)
        Rails.logger.info "throttled response: temporary adding #{ip} to "\
        "blocklist_ips"
        Notify.exception_with_env(
          "ip_throttle",
          env: env,
          email_prefix: "just info that #{ip} is on blocklist for some time",
          data: {
            help_text: "You can remove this temporary block in rails console "\
            "with this command (note that throttle still works):
            rake rack_attack:remove_from_blocklist_ips[#{ip}]
            ')" }
        )
      else
        Rails.logger.info "throttled response, but do not know ip address"
      end

      # Using 503 because it may make attacker think that they have successfully
      # DOSed the site. Rack::Attack returns 429 for throttling by default
      # [ 503, {}, [body]]
      [503, {}, ["Try later"]]
    end

    def self.find_ip_from_env(env)
      if env["REMOTE_ADDR"].present?
        env["REMOTE_ADDR"]
      else
        # heroku
        env["HTTP_X_FORWARDED_FOR"]
      end
    end
  end
end

In Rspect you can test with

RSpec.describe 'Rack::Attack' do
  describe 'disable user agents' do
    it 'nimbostratus' do
      get '/', params: {}, headers: { 'HTTP_USER_AGENT': 'Mozilla/5.0 (compatible; Nimbostratus-Bot/v1.3.2; http://cloudsystemnetworks.com) Nimbostratus' }
      expect(response.body).to eq 'Your IP address (127.0.0.1) has been blocked. If you think that this a mistake please write to [email protected]'
    end
  end
end

You can test with command:

ab -n 350 http://localhost:3000

Default rails cache is file, so you need to rm -rf tmp/cache if you want to test clean situation.

If you receive too much 404 in logs, you can ban ip addreses http://marianposaceanu.com/articles/keep-your-rails-logs-free-of-unwanted-noise For example block ip address for one day if request is made to /{wp-admin|wp-login}

# After 1 blocked requests in 10 minutes, block all requests from that IP for 1 day.
Rack::Attack.blocklist('fail2ban pentesters') do |req|
  # `filter` returns truthy value if request fails, or if it's from a previously banned IP so the request is blocked
  Rack::Attack::Fail2Ban.filter(
    "pentesters-#{req.ip}",
    maxretry: 1,
    findtime: 10.minutes,
    bantime: 1.day
  ) do
    # The count for the IP is incremented if the return value is truthy
    req.path.include?('wp-admin') ||
    req.path.include?('wp-login')
  end
end

Rack Timeout

You can use rack-timeout gem to abort requests that takes too long. For example on Heroku there is a limit of 30seconds and longer requests are simply canceled (there is no Expection notification in this case). Solution is to manually cancel requests that takes 29 seconds and use 1 second to send notification. https://github.com/heroku/rack-timeout To resolve this problem you should but those requests to background jobs.

SQL Injection

  • rails-sqli list of common injections
  • external params should be sanitized where("id = ?", params[:id]) or where("id = :id", id: params[:id])
  • with order we can not use sanitizations ? or :id method. So do not use string interpolation since anybody can do anything (drop table, get access). You can try with adding limit 1# (# is %23 encoded) for example ?sort=created_at&direction=asc+limit+1%23. Solution is to use methods that returns only column names

    # app/controllers/application_controller.rb
    def sort_column
      %w[id name created_at].include?(params[:sort])) ? params[:sort] : "id"
    end
    
    def sort_direction
      %w[asc desc].include?(params[:direction]) ?  params[:direction] : "desc"
    end
    
    def sort_query
      "#{sort_column} #{sort_direction}"
    end
    
    # use like: User.order(sort_query)
    

SSL

Add free ssl

  • Test if it is working https://www.ssllabs.com/ssltest/ try config with Mozzila ssl config generator https://mozilla.github.io/server-side-tls/ssl-config-generator/

  • Add your site to hsts list https://hstspreload.org/ so major browser will use https instead http (can’t use http).
  • If not using https than ISP or Browser can easilly inject javascript into the page and show adds.
  • If you have password field on a page, browsers will require https or show a “Not Secure Warning”.
  • It is mandatory for HTTP 2.0.
  • Some browsers features are only available over HTTPS: geolocation, service workers, fullscreen…

CSP

https://www.randomerrata.com/articles/2017/rails-security/ CSP content security policy is header that tells browser where assets can be loaded from. Rails initialy gives us protection from: CSRF, XSS, Injection (SQL, HTML, Javascript). Default headers: X-Frame-Options, X-XSS-Protection, X-Content-Type-Options.

Gem https://github.com/twitter/secureheaders can be added (just add to your Gemfile gem 'secure_headers' and create configuration

# config/initializers/secure_headers.rb
SecureHeaders::Configuration.default do |config|
end
Content-Security-Policy:
# allow only scripts from the same origin as the page
script-src 'self'
# allow only scripts from the same origin as the page and google
script-src 'self' https://apis.google.com
# allow all sources but disallow unsafe inline assets
script-src *
# allow all and allow inline
script-src * 'unsafe-inline'

# reporting to
report-uri https://my-domain.report-uri.io/r/default/csp/reportOnly

Content-Security-Policy-Report-Only

https://github.com/gbataille/csp_report You can deploy to production using Report only so you can see what assets are used and add them one by one.

You can test if browser is injecting js https://gist.github.com/danharper/8364399

Tutorial https://chase.pursu.es/simple-intro-to-csp-for-rails.html

Iptables

There are 3 types of chains: input, forward and output (some services like ssh use both input and output chains). Default policy (for example ACCEPT) is used for unmatched traffic. Response could be Accept, Drop (do not notify the source) and Reject (source knows that it was rejected).

# list all rules
iptables -L
# block all connections from 10.10.10.10
iptables -A INPUT -s 10.10.10.10 -j DROP
# block all connections from 10.10.10.10 for ssh
iptables -A INPUT -p tcp --dport ssh -s 10.10.10.10 -j DROP

# SSH connections FROM 10.10.10.10 are permitted, but SSH TO 10.10.10.10 are not
iptables -A INPUT -p tcp --dport ssh -s 10.10.10.10 -m state --state NEW,ESTABLISHED -j ACCEPT
iptables -A OUTPUT -p tcp --sport 22 -d 10.10.10.10 -m state --state ESTABLISHED -j ACCEPT

iptables-save to save changes.

Example for WordPress useragent verifying pingback from ddos attack https://www.christophe-casalegno.com/under-wordpress-pingback-ddos-attack-use-iptables/

Monit

https://www.youtube.com/watch?v=wiRt3mY7Rrw Monitor proccesses, if using too many resources (cycles and memory), network http 80, or any script result.

# read configuration with verbose
sudo monit -v
# stop and start, but better is force reload
sudo service monit force-reload
# similar to web
sudo monit summary

Syntax man monit

  • You can use noise keywords like ‘if’, ‘and’, ‘with(in)’, ‘has’, ‘us(ing|e)’, ‘on(ly)’, ‘then’, ‘for’, ‘of’
  • set name globbal set statement
  • include file_name include directivers from file
  • check ... service entry statements, 9 types
    • check process <name> pidfile <path> | matching <regex>
    • check file <name> path <path>
    • check host <name> address <host>
    • check system <name>
    • check program <name> path <executable file>
    • also fifo, filesystem, directory,
  • ALERT MESSAGES set alert recipients. SET ALERT mail-address [[NOT] {event, ...}] [REMINDER cycles]
  • SERVICE METHODS each service can have start| stop|restart than program = '/usr/local/path' and as uid 'ubuntu' and with timeout 60 seconds default timeout is 30 seconds
  • SERVICE DEPENDENCIES depends on <name> is used to check dependency before start/stop of service. Use check file or check program and restart to actually restart service that depends on it.
  • SERVICE TESTS are if tests inside check statement, syntax is IF <test> THEN <action> [ELSE IF SUCCEEDED THEN <action>] action can be alert, restart, start, stop. tests: exists or not exists for all types RESOURCE TESTS only within system (loadavg, cpu, memory, swap) and proccess service entry cpu, total cpu, memory, total memory. FILE CHECKSUM TESTS only for file service entry if changed checksum then SPACE USAGE TESTS only for filesystem service type space usage > 80% … NETWORK INTERFACE TESTS NETWORK PING TEST only for check host statement CONNECTION TEST for process and host service type
    check host my-staging.com
      if failed port 80 protocol http content "Service Provider" for 2 cycles then alert
    
    

For background jobs I use three times the same check without any dependency

# https://serverfault.com/questions/662422/how-to-watch-a-service-with-multiple-processes-with-monit
# if we define same proccess three times for three workers than when it needs to
# stop/start it will do three times, so we will check only first one for memory
# and cpu
check process delayed_job_0
  with pidfile /home/ubuntu/xceednet/current/tmp/pids/delayed_job.0.pid
  start program = "/bin/bash -l -c 'cd /home/ubuntu/xceednet/current && RAILS_ENV=staging /home/ubuntu/.rbenv/bin/rbenv exec bundle exec cap staging locally:delayed_job:start ROLES=worker'" as guid "ubuntu" and uid "ubuntu"
  stop program = "/bin/bash -l -c 'cd /home/ubuntu/xceednet/current && RAILS_ENV=staging /home/ubuntu/.rbenv/bin/rbenv exec bundle exec cap staging locally:delayed_job:stop ROLES=worker'" as guid "ubuntu" and uid "ubuntu"
  restart program = "/bin/bash -l -c 'cd /home/ubuntu/xceednet/current && RAILS_ENV=staging /home/ubuntu/.rbenv/bin/rbenv exec bundle exec cap staging locally:delayed_job:restart ROLES=worker'" as guid "ubuntu" and uid "ubuntu"
  if totalmem > 400.0 MB for 3 cycles then restart
  if cpu usage > 95% for 3 cycles then restart

check file delayed_job_1
  with path /home/ubuntu/xceednet/current/tmp/pids/delayed_job.1.pid

Check file system usage hard drive

# https://serverfault.com/a/1048594
check device root with path /
  if SPACE usage > 85% then alert

For local server you can trigger unmonitor and monitor action for services

sudo monit status

# when monitoring status is Not monitored than no alerts is send when status is
# changed. It does not mean that the process is not running, it is just not
# monitored

Process 'delayed_job_1'
  status                       OK / Does not exists / Resource limit matched
  monitoring status            Monitored / Not monitored
  monitoring mode              active
  on reboot                    start
  pid                          23891
  parent pid                   1
  uid                          1000
  effective uid                1000
  gid                          1000
  uptime                       22m
  threads                      3
  children                     0
  cpu                          0.0%
  cpu total                    0.0%
  memory                       11.1% [220.3 MB]
  memory total                 11.1% [220.3 MB]
  security attribute           unconfined
  disk write                   0 B/s [164 kB total]
  data collected               Mon, 21 Dec 2020 23:03:08

Sample ruby project for monitor https://github.com/karmi/monittr but not updated 10 years

http://www.netpingdevice.com/blog/primer-monitoringa-servernoj-komnaty-na-osnove-monit-influxdb-grafana-i-ustrojstv-netping https://stackoverflow.com/questions/60277886/how-to-heroku-style-app-monitoring-in-dokku

Sample security app

https://github.com/OWASP/railsgoat/wiki/Rails-5-Tutorials

Running sinatraa on mobile phone

https://lbrito1.github.io/blog/2020/02/repurposing-android.html

Tips

  • use scoped find, instead of Order.find params[:id] use current_user.orders find params[:id]
  • use rate limiting, for example facebook password recovery number is 6 digits, and beta.facebook did not have rate limit, so you can try 1 milin numbers very quickly
  • do not trust the mobile app, for example mobile app make a failed payment and than change the payment response to be success and send that to server
  • if your server is doing curl requests for some user url, than user can set up sftp or smtp requests to any server and hide itself
  • use SHA 256 instead md5 and SHA 1

https://github.com/eliotsykes/rails-security-checklist https://github.com/eliotsykes/real-world-rails https://github.com/pxlpnk/awesome-ruby-security