rack-shield 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 91381e561f753758f33de1f296eaf6df921d131d8780ec5d3ca6d21a1cabc022
4
+ data.tar.gz: 737b223d0e6990ad9447f8f82b725d4a0e7bdfed9dc53d59085d78363c9d2e8b
5
+ SHA512:
6
+ metadata.gz: 31d9e273c7837fa8e6626247b1cc361e5f4138cf3061cc0c8fc605b717e962e48272d17b00ab3afd36374710422e4bb340322562835522c66ee7103c938b9870
7
+ data.tar.gz: cfe65f88d463b9874d3e7b01045171c909fa74bede02f98b79bf5f57a32b3b648e43ee82d7c897b817ee11b95ffa6742c6e55a2663acc31809cf6f3d147449d0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Matthias Grosser
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ ![Shield](https://raw.githubusercontent.com/mtgrosser/rack-shield/master/doc/shield.svg)
2
+
3
+ # Rack::Shield
4
+
5
+ Simple frontend to block and unblock evil requests with `Rack::Attack`
6
+
7
+ ## Installation
8
+
9
+ In your Gemfile:
10
+
11
+ ```ruby
12
+ gem 'rack-attack-shield'
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ Check whether request is evil:
18
+
19
+ ```ruby
20
+ Rack::Shield.evil?(request)
21
+ ```
22
+
23
+ With `Rack::Attack::Fail2Ban`:
24
+
25
+ ```ruby
26
+ # After one blocked request in 10 minutes, block all requests from that IP for 5 minutes.
27
+ Rack::Attack.blocklist('fail2ban pentesters') do |req|
28
+ Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", maxretry: 1, findtime: 10.minutes, bantime: 5.minutes) do
29
+ Rack::Shield.evil?(req)
30
+ end
31
+ end
32
+ ```
33
+
34
+ ## Configuration
35
+
36
+ Adding to path matchers:
37
+
38
+ ```ruby
39
+ # Regexp will be matched
40
+ Rack::Shield.evil_paths << /\.sql\z/
41
+
42
+ # String will be checked for inclusion
43
+ Rack::Shield.evil_paths << '/wp-admin'
44
+ ```
45
+ Defaults are defined in `Rack::Shield::DEFAULT_EVIL_PATHS`.
46
+
47
+ ## Blocked response
48
+
49
+ By default, the blocked response is generated automatically:
50
+
51
+ ```ruby
52
+ # default
53
+ Rack::Shield.response = Rack::Shield::Response
54
+ ```
55
+
56
+ It can be set to any `call`able object which conforms to the `Rack` interface:
57
+
58
+ ```ruby
59
+ Rack::Shield.response = ->(env) { [403, { 'Content-Type' => 'text/html' }, ["Blocked!\n"]]
60
+ ```
61
+
62
+ In Rails apps, the blocked response will be generated from `app/views/layouts/shield.html`.
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ t.warning = false
9
+ end
10
+
11
+ task :default => :test
12
+
13
+ namespace :rack do
14
+ namespace :shield do
15
+ task :install do
16
+ require 'rack/shield'
17
+ file = defined?(Rails) ? Rails.root.join('app', 'views', 'layouts', 'shield.html') : 'shield.html'
18
+ if file.exist?
19
+ puts "skipping #{file}"
20
+ else
21
+ file.write(Rack::Shield::Response.default_template.read)
22
+ puts "create #{file}"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ module Rack
2
+ module Shield
3
+ module RequestExt
4
+ def raw_post_data
5
+ env['RAW_POST_DATA']
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ Rack::Attack::Request.include Rack::Shield::RequestExt
@@ -0,0 +1,51 @@
1
+ module Rack
2
+ module Shield
3
+ class Responder
4
+ attr_reader :request
5
+
6
+ class << self
7
+ attr_writer :template
8
+
9
+ def template
10
+ @template ||= if defined?(Rails)
11
+ Rails.root.join('app', 'views', 'layouts', 'shield.html')
12
+ else
13
+ default_template
14
+ end
15
+ end
16
+
17
+ def default_template
18
+ Pathname.new(__FILE__).dirname.join('..', '..', '..', '..', 'templates', 'shield.html')
19
+ end
20
+
21
+ def call(request)
22
+ new(request).render
23
+ end
24
+ end
25
+
26
+ def initialize(request)
27
+ @request = request
28
+ end
29
+
30
+ def env
31
+ request.env
32
+ end
33
+
34
+ def render
35
+ return [403, { 'Content-Type' => 'text/html' }, []] if head?
36
+ html = self.class.template.read.gsub('%REQUEST_IP%', request_ip.to_s)
37
+ [403, { 'Content-Type' => 'text/html' }, ["#{html}\n"]]
38
+ end
39
+
40
+ private
41
+
42
+ def request_ip
43
+ env['HTTP_X_REAL_IP'] || env['REMOTE_ADDR']
44
+ end
45
+
46
+ def head?
47
+ request.head?
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ module Shield
3
+ VERSION = '1.1.0'
4
+ end
5
+ end
@@ -0,0 +1,106 @@
1
+ require 'pathname'
2
+ require 'rack/attack'
3
+ require_relative 'shield/version'
4
+ require_relative 'shield/responder'
5
+ require_relative 'shield/request_ext'
6
+
7
+ module Rack
8
+ module Shield
9
+ DEFAULT_PATHS = [/\/wp-(includes|content|admin|json|config)/,
10
+ /\.(php|cgi|asp|aspx|shtml|log|(my)?sql(\.tar)?(\.t?(gz|zip))?|cfm|py|lasso|e?rb|pl|jsp|do|action|sh)\z/i,
11
+ 'cgi-bin',
12
+ 'phpmyadmin',
13
+ '/pma/',
14
+ '/boaform/',
15
+ 'sqlbuddy',
16
+ /(my)?sql-backup/,
17
+ 'etc/passwd',
18
+ '/php/',
19
+ '.php/',
20
+ '/browsedisk',
21
+ '/mambo/',
22
+ '/jenkins/',
23
+ '/joomla/',
24
+ '/varien/js.js',
25
+ '/drupal.js',
26
+ 'RELEASE_NOTES.txt',
27
+ '/phpunit/',
28
+ '/magento/',
29
+ '/mage/',
30
+ '/magento_version',
31
+ '/mifs/',
32
+ '/js/varien/',
33
+ '/includes/',
34
+ '/HNAP1',
35
+ '/stalker_portal/',
36
+ '/nmaplowercheck',
37
+ '/solr/admin/',
38
+ '/axis2/axis2-admin',
39
+ '/telescope/requests',
40
+ '/RELEASE_NOTES.txt',
41
+ 'deployment-config.json',
42
+ 'ftpsync.settings',
43
+ '/_profiler/latest',
44
+ '/_ignition/execute-solution',
45
+ '/_wpeprivate/',
46
+ '/Config/SaveUploadedHotspotLogoFile',
47
+ 'ALFA_DATA',
48
+ 'cgialfa',
49
+ 'alfacgiapi',
50
+ /\A\/"/,
51
+ /\/\.(hg|git|svn|bzr|htaccess|ftpconfig|vscode|remote-sync|aws|env|DS_Store)/,
52
+ /\/old\/?\z/,
53
+ /\/\.env\z/,
54
+ /\A\/old-wp/,
55
+ /\A\/(wordpress|wp)(\/|\z)/]
56
+
57
+ DEFAULT_QUERIES = [/SELECT.+FROM.+/i,
58
+ /SELECT.+COUNT/i,
59
+ /SELECT.+UNION/i,
60
+ /UNION.+SELECT/i,
61
+ /INFORMATION_SCHEMA/i,
62
+ '--%20',
63
+ '-- ',
64
+ '%2Fscript%3E',
65
+ '<script>', '</script>',
66
+ '<php>', '</php>',
67
+ 'XDEBUG_SESSION_START',
68
+ 'phpstorm',
69
+ '<php>',
70
+ 'onload=confirm',
71
+ 'HelloThinkCMF',
72
+ 'XDEBUG_SESSION_START',
73
+ ]
74
+
75
+ class << self
76
+
77
+ attr_accessor :paths, :queries, :checks, :responder
78
+
79
+ def evil?(req)
80
+ (req.path && paths.any? { |matcher| match?(req.path, matcher) }) ||
81
+ (req.query_string && queries.any? { |matcher| match?(req.query_string, matcher) }) ||
82
+ (checks.any? { |matcher| match?(req, matcher) })
83
+ end
84
+
85
+ def template
86
+ Pathname.new(__FILE__).dirname.join('..', '..', '..', 'templates', 'shield.html')
87
+ end
88
+
89
+ private
90
+
91
+ def match?(obj, matcher)
92
+ case matcher
93
+ when String then obj.include?(matcher)
94
+ when Regexp then obj.match?(matcher)
95
+ when Proc then matcher.call(obj)
96
+ end
97
+ end
98
+ end
99
+
100
+ self.paths = DEFAULT_PATHS.dup
101
+ self.queries = DEFAULT_QUERIES.dup
102
+ self.checks = []
103
+ self.responder = Responder
104
+
105
+ end
106
+ end
@@ -0,0 +1 @@
1
+ require 'rack/shield'
@@ -0,0 +1,146 @@
1
+ <!DOCTYPE html>
2
+
3
+ <html>
4
+
5
+ <head>
6
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
7
+ <title>Access blocked</title>
8
+ <link rel="icon" href="data:,">
9
+ <style type="text/css">
10
+ @import 'https://fonts.googleapis.com/css?family=Roboto:300,500';
11
+
12
+ body {
13
+ background-color: white;
14
+ color: #666;
15
+ text-align: center;
16
+ font-family: 'Roboto', sans-serif;
17
+ font-weight: 300;
18
+ font-size: 18px;
19
+ line-height: 1.5em;
20
+ }
21
+
22
+ div#centerbox {
23
+ display: block;
24
+ position: absolute;
25
+ width: 100%;
26
+ height: 1px;
27
+ top: 50%;
28
+ left: 0px;
29
+ overflow: visible;
30
+ visibility: visible;
31
+ }
32
+
33
+ div#centerbox #dialog {
34
+ position: absolute;
35
+ width: 600px;
36
+ height: 500px;
37
+ top: -250px;
38
+ left: 50%;
39
+ margin-left: -300px;
40
+ overflow:visible;
41
+ }
42
+
43
+ div.dialog {
44
+ width: 25em;
45
+ padding: 0 4em;
46
+ margin: auto;
47
+ //border: 1px solid #ccc;
48
+ border-right-color: #999;
49
+ border-bottom-color: #999;
50
+ }
51
+
52
+ h1 {
53
+ font-size: 40px;
54
+ font-weight: 100;
55
+ line-height: 1.5em;
56
+ }
57
+
58
+ a {
59
+ color: #428aff;
60
+ text-decoration: none;
61
+ }
62
+
63
+ #checkbox-unchecked, #checkbox-checked {
64
+ width: 40px;
65
+ height: 40px;
66
+ vertical-align: middle;
67
+ }
68
+
69
+ #checkbox-checked {
70
+ display: none;
71
+ }
72
+
73
+ .norobot {
74
+ line-height: 45px;
75
+ }
76
+ </style>
77
+ </head>
78
+
79
+ <body class="blocked-page">
80
+ <div id="centerbox">
81
+ <div id="dialog">
82
+ <img id="logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzJkNjhjNCIgc3Ryb2tlLXdpZHRoPSIyLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgY2xhc3M9ImZlYXRoZXIgZmVhdGhlci1zaGllbGQiPjxwYXRoIGQ9Ik0xMiAyMnM4LTQgOC0xMFY1bC04LTMtOCAzdjdjMCA2IDggMTAgOCAxMHoiPjwvcGF0aD48L3N2Zz4=" alt="Shield"/>
83
+ <br/><br/>
84
+ <div lang="de">
85
+ <h1>Diese Webseite ist geschützt.</h1>
86
+ <p>
87
+ Wir haben eine ungewöhnliche Aktivität von Ihrer IP-Adresse <strong>%REQUEST_IP%</strong> festgestellt und den Zugriff auf diese Webseite blockiert.<br/>
88
+ <br/>
89
+ <strong>Bitte bestätigen Sie, dass Sie kein Roboter sind:</strong>
90
+ </p>
91
+ </div>
92
+ <div lang="en" style="display: none;">
93
+ <h1>This website is protected.</h1>
94
+ <p>
95
+ We have noticed an unusual activity from your IP <strong>%REQUEST_IP%</strong> and blocked access to this website.<br/>
96
+ <br/>
97
+ <strong>Please confirm that you are not a robot:</strong>
98
+ </p>
99
+ </div>
100
+ <div>
101
+ <p>
102
+ <img id="checkbox-unchecked" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM2NjY2NjYiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0iZmVhdGhlciBmZWF0aGVyLXNxdWFyZSI+PHJlY3QgeD0iMyIgeT0iMyIgd2lkdGg9IjE4IiBoZWlnaHQ9IjE4IiByeD0iMiIgcnk9IjIiPjwvcmVjdD48L3N2Zz4=">
103
+ <img id="checkbox-checked" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM2NjY2NjYiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0iZmVhdGhlciBmZWF0aGVyLWNoZWNrLXNxdWFyZSI+PHBvbHlsaW5lIHBvaW50cz0iOSAxMSAxMiAxNCAyMiA0Ij48L3BvbHlsaW5lPjxwYXRoIGQ9Ik0yMSAxMnY3YTIgMiAwIDAgMS0yIDJINWEyIDIgMCAwIDEtMi0yVjVhMiAyIDAgMCAxIDItMmgxMSI+PC9wYXRoPjwvc3ZnPg==">
104
+ &nbsp;<span class="norobot" lang="de">Ich bin kein Roboter.</span><span class="norobot" lang="en" style="display: none;">I'm not a robot.</span>
105
+ <input type="hidden" name="foobar">
106
+ </p>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ <script>
111
+ var token = '%TOKEN%';
112
+
113
+ var hide = function(elt) {
114
+ elt.style.display = 'none';
115
+ };
116
+
117
+ var unhide = function(elt) {
118
+ switch (elt.tagName.toLowerCase()) {
119
+ case 'span':
120
+ case 'img':
121
+ elt.style.display = 'inline';
122
+ break;
123
+ default:
124
+ elt.style.display = 'block';
125
+ }
126
+ };
127
+
128
+ var language = window.navigator.userLanguage || window.navigator.language;
129
+ if ( language && language.toLowerCase().startsWith('en') ) {
130
+ document.querySelectorAll('[lang=de]').forEach((elt) => { elt.style.display = 'none' });
131
+ document.querySelectorAll('[lang=en]').forEach(unhide);
132
+ }
133
+
134
+ var unchecked = document.getElementById('checkbox-unchecked');
135
+ var checked = document.getElementById('checkbox-checked');
136
+ unchecked.addEventListener('click', function() {
137
+ hide(unchecked);
138
+ unhide(checked);
139
+ });
140
+ checked.addEventListener('click', function() {
141
+ hide(checked);
142
+ unhide(unchecked);
143
+ });
144
+ </script>
145
+ </body>
146
+ </html>
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-shield
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthias Grosser
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-08-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack-attack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 6.6.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 6.6.0
27
+ description: Plugin for rack-attack to block and unblock evil requests
28
+ email:
29
+ - mtgrosser@gmx.net
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - lib/rack-shield.rb
38
+ - lib/rack/shield.rb
39
+ - lib/rack/shield/request_ext.rb
40
+ - lib/rack/shield/responder.rb
41
+ - lib/rack/shield/version.rb
42
+ - templates/shield.html
43
+ homepage: https://github.com/mtgrosser/rack-shield
44
+ licenses:
45
+ - MIT
46
+ metadata: {}
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.1.4
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: Block and unblock evil requests
66
+ test_files: []