Skip to content

Commit a21b994

Browse files
authored
Merge pull request #209 from MITLibraries/timx-480-rack-attack-config
Adds rack-attack throttles
2 parents 5babd3f + e12a1a5 commit a21b994

File tree

2 files changed

+131
-0
lines changed

2 files changed

+131
-0
lines changed

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ Redis to track requests.
8383

8484
See `Optional Environment Variables` for more information.
8585

86+
### Rack Attack
87+
88+
This application uses [Rack Attack](https://github.com/rack/rack-attack).
89+
90+
See `Optional Environment Variables` for more information.
91+
8692
### Required Environment Variables
8793

8894
- `TIMDEX_GRAPHQL`: Set this to the URL of the GraphQL endpoint. There is no default value in the application.
@@ -115,6 +121,10 @@ may have unexpected consequences if applied to other TIMDEX UI apps.
115121
- `GLOBAL_ALERT`: The main functionality for this comes from our theme gem, but when set the value will be rendered as
116122
safe html above the main header of the site.
117123
- `PLATFORM_NAME`: The value set is added to the header after the MIT Libraries logo. The logic and CSS for this comes from our theme gem.
124+
- `REQUESTS_PER_PERIOD` - number of requests that can be made for general throttles per `REQUEST_PERIOD`
125+
- `REQUEST_PERIOD` - time in minutes used along with `REQUESTS_PER_PERIOD`
126+
- `REDIRECT_REQUESTS_PER_PERIOD`- number of requests that can be made that the query string starts with our legacy redirect parameter to throttle per `REQUEST_PERIOD`
127+
- `REDIRECT_REQUEST_PERIOD`- time in minutes used along with `REDIRECT_REQUEST_PERIOD`
118128
- `SENTRY_DSN`: Client key for Sentry exception logging.
119129
- `SENTRY_ENV`: Sentry environment for the application. Defaults to 'unknown' if unset.
120130
- `TIMDEX_INDEX`: Name of the index, or alias, to provide to the GraphQL endpoint. Defaults to `nil` which will let TIMDEX determine the best index to use. Wildcard values can be set, for example `rdi*` would search any indexes that begin with `rdi` in the underlying OpenSearch instance behind TIMDEX.

config/initializers/rack_attack.rb

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
class Rack::Attack
2+
3+
### Configure Cache ###
4+
5+
# If you don't want to use Rails.cache (Rack::Attack's default), then
6+
# configure it here.
7+
#
8+
# Note: The store is only used for throttling (not blocklisting and
9+
# safelisting). It must implement .increment and .write like
10+
# ActiveSupport::Cache::Store
11+
12+
# Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
13+
14+
### Safelist MIT IP addresses
15+
# http://kb.mit.edu/confluence/x/F4DCAg
16+
# Main IP range (includes campus, NAT pool, and VPNs)
17+
# This also affects bot_challenge_page logic which uses rack_attack under the hood
18+
Rack::Attack.safelist_ip("18.0.0.0/11")
19+
20+
### Throttle Spammy Clients ###
21+
22+
# If any single client IP is making tons of requests, then they're
23+
# probably malicious or a poorly-configured scraper. Either way, they
24+
# don't deserve to hog all of the app server's CPU. Cut them off!
25+
#
26+
# Note: If you're serving assets through rack, those requests may be
27+
# counted by rack-attack and this throttle may be activated too
28+
# quickly. If so, enable the condition to exclude them from tracking.
29+
30+
# Throttle all requests by IP (default is 100 requests per 10 minutes)
31+
#
32+
# Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}"
33+
throttle('req/ip',
34+
limit: (ENV.fetch('REQUESTS_PER_PERIOD') { 100 }).to_i,
35+
period: (ENV.fetch('REQUEST_PERIOD') { 10 }).to_i.minutes) do |req|
36+
# don't include assets as requests
37+
req.ip unless req.path.start_with?('/assets')
38+
end
39+
40+
# Throttle redirects by IP (default is 5 per 10 minutes)
41+
#
42+
# Key: "rack::attack:#{Time.now.to_i/:period}:req/ip/redirects:#{req.ip}"
43+
throttle('req/ip/redirects',
44+
limit: (ENV.fetch('REDIRECT_REQUESTS_PER_PERIOD') { 5 }).to_i,
45+
period: (ENV.fetch('REDIRECT_REQUEST_PERIOD') { 10 }).to_i.minutes) do |req|
46+
req.ip if req.query_string.start_with?('geoweb-redirect')
47+
end
48+
49+
### Prevent Brute-Force Login Attacks ###
50+
51+
# The most common brute-force login attack is a brute-force password
52+
# attack where an attacker simply tries a large number of emails and
53+
# passwords to see if any credentials match.
54+
#
55+
# Another common method of attack is to use a swarm of computers with
56+
# different IPs to try brute-forcing a password for a specific account.
57+
58+
# Throttle POST requests to /login by IP address
59+
#
60+
# Key: "rack::attack:#{Time.now.to_i/:period}:logins/ip:#{req.ip}"
61+
# throttle('logins/ip', limit: 5, period: 20.seconds) do |req|
62+
# if req.path == '/login' && req.post?
63+
# req.ip
64+
# end
65+
# end
66+
67+
# Throttle POST requests to /login by email param
68+
#
69+
# Key: "rack::attack:#{Time.now.to_i/:period}:logins/email:#{req.email}"
70+
#
71+
# Note: This creates a problem where a malicious user could intentionally
72+
# throttle logins for another user and force their login requests to be
73+
# denied, but that's not very common and shouldn't happen to you. (Knock
74+
# on wood!)
75+
# throttle("logins/email", limit: 5, period: 20.seconds) do |req|
76+
# if req.path == '/login' && req.post?
77+
# # return the email if present, nil otherwise
78+
# req.params['email'].presence
79+
# end
80+
# end
81+
82+
### Custom Throttle Response ###
83+
84+
# By default, Rack::Attack returns an HTTP 429 for throttled responses,
85+
# which is just fine.
86+
#
87+
# If you want to return 503 so that the attacker might be fooled into
88+
# believing that they've successfully broken your app (or you just want to
89+
# customize the response), then uncomment these lines.
90+
# self.throttled_response = lambda do |env|
91+
# [ 503, # status
92+
# {}, # headers
93+
# ['']] # body
94+
# end
95+
96+
# Block suspicious requests for '/etc/password' or wordpress specific paths.
97+
# After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes.
98+
# Note: these will not show up in the throttle logs as they are blocks and not throttles
99+
Rack::Attack.blocklist('fail2ban pentesters') do |req|
100+
# `filter` returns truthy value if request fails, or if it's from a previously banned IP
101+
# so the request is blocked
102+
Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", maxretry: 3, findtime: 10.minutes, bantime: 5.minutes) do
103+
# The count for the IP is incremented if the return value is truthy
104+
CGI.unescape(req.query_string) =~ %r{/etc/passwd} ||
105+
req.path.include?('/etc/passwd') ||
106+
req.path.include?('wp-admin') ||
107+
req.path.include?('wp-login')
108+
end
109+
end
110+
111+
# Log when throttles are triggered
112+
ActiveSupport::Notifications.subscribe("throttle.rack_attack") do |name, start, finish, request_id, payload|
113+
@@rack_logger ||= ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
114+
@@rack_logger.info{[
115+
"[#{payload[:request].env['rack.attack.match_type']}]",
116+
"[#{payload[:request].env['rack.attack.matched']}]",
117+
"[#{payload[:request].env['rack.attack.match_discriminator']}]",
118+
"[#{payload[:request].env['rack.attack.throttle_data']}]",
119+
].join(' ') }
120+
end
121+
end

0 commit comments

Comments
 (0)