Web Vulnerability And Dos Attacks
Contents |
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 fromCopy 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&
,<
to<
,>
to>
, quotation mark"
to"
(
hex&34;
dec), apostrophe'
to&x27;
(&39;
decimal),/
to/
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 stress test:
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])
orwhere("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 addinglimit 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 server reply with
Strict-Transport-Security: max-age=16070400; includeSubDomains
than browser will always use https source. For initial request browser maintains “hsts preload list” https://hstspreload.org where you can submit your sites google owns .dev and put it on hsts list so it will use https instead http formy-domain.dev
.
- if server reply with
- 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 statementinclude file_name
include directivers from filecheck ...
service entry statements, 9 typescheck 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
thanprogram = '/usr/local/path'
andas uid 'ubuntu'
andwith timeout 60 seconds
default timeout is 30 seconds - SERVICE DEPENDENCIES
depends on <name>
is used to check dependency before start/stop of service. Usecheck file
orcheck program
andrestart
to actually restart service that depends on it. - SERVICE TESTS are
if
tests insidecheck
statement, syntax isIF <test> THEN <action> [ELSE IF SUCCEEDED THEN <action>]
action can bealert
,restart
,start
,stop
. tests:exists
ornot exists
for all types RESOURCE TESTS only within system (loadavg
,cpu
,memory
,swap
) and proccess service entrycpu
,total cpu
,memory
,total memory
. FILE CHECKSUM TESTS only for file service entryif changed checksum then
SPACE USAGE TESTS only for filesystem service typespace usage > 80%
… NETWORK INTERFACE TESTS NETWORK PING TEST only for check host statement CONNECTION TEST for process and host service typecheck 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]
usecurrent_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
- do not use secrets (api keys or passwords) in url https://w3ctag.github.io/capability-urls/