diff --git a/README.md b/README.md
index ff58d594..b8bd9a47 100644
--- a/README.md
+++ b/README.md
@@ -107,6 +107,12 @@ Professional support for Rdiffweb is available by contacting [IKUS Soft](https:/
# Changelog
+## 2.4.4 (2002-09-15)
+
+This releases include a security fix. If you are using an earlier version, you should upgrade to this release immediately.
+
+* Use `X-Real-IP` to identify client IP address to mitigate Brute-Force attack #213
+
## 2.4.3 (2022-09-14)
This releases include a security fix. If you are using an earlier version, you should upgrade to this release immediately.
@@ -237,7 +243,7 @@ Maintenance release to fix minor issues
* Fix to retrieve user quota only for valid user_root #135
* Add option `disable-ssh-keys` to disable SSH Key management
* Use absolute URL everywhere
- * Add support for `X-Forward-For`, `X-Forward-proto` and other reverse proxy header when generating absolute URL
+ * Add support for `X-Forwarded-For`, `X-Forwarded-proto` and other reverse proxy header when generating absolute URL
* Drop Debian Stretch support
* Implement a new background scheduler using apscheduler #82
* Use background job to send email notification to avoid blocking web page loading #47
diff --git a/doc/networking.md b/doc/networking.md
index fb649dba..8362eb4b 100644
--- a/doc/networking.md
+++ b/doc/networking.md
@@ -4,8 +4,8 @@
You may need an Apache server in case:
- * you need to serve multiple web services from the same IP;
- * you need more security (like HTTP + SSL).
+* you need to serve multiple web services from the same IP;
+* you need more security (like HTTP + SSL).
This section doesn't explain how to install and configure your Apache server.
This is out-of-scope. The following is only provided as a suggestion and is in
@@ -21,13 +21,14 @@ no way a complete reference.
**Basic configuration**
-Add the following to your Apache configuration. It's recommended to create a
+Add the following to your Apache configuration. It's recommended to create a
file in `/etc/apache2/sites-available/rdiffweb`.
ServerName rdiffweb.mydomain.com
ProxyPass / http://localhost:8080/ retry=5
ProxyPassReverse / http://localhost:8080/
+ RemoteIPHeader X-Real-IP
**SSL configuration**
@@ -37,7 +38,7 @@ Here is an example with SSL configuration.
ServerName rdiffweb.mydomain.com
ServerAdmin me@mydomain.com
- # TODO Redirect HTTP to HTTPS
+ # Redirect HTTP to HTTPS
RewriteEngine on
RewriteRule ^(.*)$ https://rdiffweb.mydomain.com$1 [L,R=301]
@@ -50,10 +51,10 @@ Here is an example with SSL configuration.
ServerName rdiffweb.mydomain.com
ServerAdmin me@mydomain.com
- # Hostaname resolution in /etc/hosts
ProxyPass / http://localhost:8080/ retry=5
ProxyPassReverse / http://localhost:8080/
RequestHeader set X-Forwarded-Proto https
+ RemoteIPHeader X-Real-IP
# SSL Configuration
SSLEngine on
@@ -64,15 +65,17 @@ Here is an example with SSL configuration.
Allow from all
-
-Take special care of `RequestHeader set X-Forwarded-Proto https` setting used to pass the right protocol to rdiffweb.
+
+Make sure you set `X-Real-IP` so that Rdiffweb knows the real IP address of the client. This is used in the rate limit to identify the client.
+
+Make sure you set `X-Forwarded-Proto` correctly so that Rdiffweb knows that access is being made using the `https` scheme. This is used to correctly create the URL on the page.
## Configure Rdiffweb behind nginx reverse proxy
You may need a nginx server in case:
- * you need to serve multiple web services from the same IP;
- * you need more security (like HTTP + SSL).
+* you need to serve multiple web services from the same IP;
+* you need more security (like HTTP + SSL).
This section doesn't explain how to install and configure your nginx server.
This is out-of-scope. The following is only provided as a suggestion and is in
@@ -90,3 +93,7 @@ no way a complete reference.
# Proxy
proxy_pass http://127.0.0.1:8080/;
}
+
+Make sure you set `X-Real-IP` so that Rdiffweb knows the real IP address of the client. This is used in the rate limit to identify the client.
+
+Make sure you set `X-Forwarded-Proto` correctly so that Rdiffweb knows that access is being made using the `https` scheme. This is used to correctly create the URL on the page.
diff --git a/rdiffweb/controller/tests/test_page_login.py b/rdiffweb/controller/tests/test_page_login.py
index 90bb6a07..e01178c2 100644
--- a/rdiffweb/controller/tests/test_page_login.py
+++ b/rdiffweb/controller/tests/test_page_login.py
@@ -277,6 +277,36 @@ def test_login_ratelimit(self):
self.assertStatus(429)
+class LoginPageRateLimitTestWithXForwardedFor(rdiffweb.test.WebCase):
+
+ default_config = {
+ 'rate-limit': 5,
+ }
+
+ def test_login_ratelimit(self):
+ # Given an unauthenticate
+ # When requesting multple time the login page
+ for i in range(0, 6):
+ self.getPage('/login/', headers=[('X-Forwarded-For', '127.0.0.%s' % i)])
+ # Then a 429 error (too many request) is return
+ self.assertStatus(429)
+
+
+class LoginPageRateLimitTestWithXRealIP(rdiffweb.test.WebCase):
+
+ default_config = {
+ 'rate-limit': 5,
+ }
+
+ def test_login_ratelimit(self):
+ # Given an unauthenticate
+ # When requesting multple time the login page
+ for i in range(0, 6):
+ self.getPage('/login/', headers=[('X-Real-IP', '127.0.0.%s' % i)])
+ # Then a 200 is return.
+ self.assertStatus(200)
+
+
class LogoutPageTest(rdiffweb.test.WebCase):
def test_getpage_without_login(self):
# Accessing logout page directly will redirect to "/".
diff --git a/rdiffweb/main.py b/rdiffweb/main.py
index 9e25729b..ef466fe0 100644
--- a/rdiffweb/main.py
+++ b/rdiffweb/main.py
@@ -46,9 +46,6 @@ def add_ip(record):
request = cherrypy.serving.request
remote = request.remote
record.ip = remote.name or remote.ip
- # If the request was forwarded by a reverse proxy
- if 'X-Forwarded-For' in request.headers:
- record.ip = request.headers['X-Forwarded-For']
return True
def add_username(record):
diff --git a/rdiffweb/rdw_app.py b/rdiffweb/rdw_app.py
index 211c7c66..137e92e0 100644
--- a/rdiffweb/rdw_app.py
+++ b/rdiffweb/rdw_app.py
@@ -38,6 +38,7 @@
import rdiffweb.tools.i18n
import rdiffweb.tools.proxy
import rdiffweb.tools.ratelimit
+import rdiffweb.tools.real_ip
import rdiffweb.tools.secure_headers
from rdiffweb.controller import Controller
from rdiffweb.controller.api import ApiPage
@@ -72,8 +73,9 @@
}
-@cherrypy.tools.proxy()
+@cherrypy.tools.proxy(remote=None)
@cherrypy.tools.secure_headers()
+@cherrypy.tools.real_ip()
class Root(LocationsPage):
def __init__(self):
self.login = LoginPage()
diff --git a/rdiffweb/tools/enrich_session.py b/rdiffweb/tools/enrich_session.py
new file mode 100644
index 00000000..cda3a868
--- /dev/null
+++ b/rdiffweb/tools/enrich_session.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# rdiffweb, A web interface to rdiff-backup repositories
+# Copyright (C) 2012-2021 rdiffweb contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+import datetime
+
+import cherrypy
+
+
+def enrich_session():
+ """
+ Store ephemeral information into user's session. e.g.: last IP address, user-agent
+ """
+ # When session is not enable, simply validate credentials
+ sessions_on = cherrypy.request.config.get('tools.sessions.on', False)
+ if not sessions_on:
+ return
+ # Get information related to the current request
+ request = cherrypy.serving.request
+ ip_address = request.remote.ip
+ cherrypy.session['ip_address'] = ip_address
+ cherrypy.session['user_agent'] = request.headers.get('User-Agent', None)
+ cherrypy.session['access_time'] = datetime.datetime.now()
+
+
+cherrypy.tools.enrich_session = cherrypy.Tool('before_handler', enrich_session, priority=60)
diff --git a/rdiffweb/tools/real_ip.py b/rdiffweb/tools/real_ip.py
new file mode 100644
index 00000000..d9a1d151
--- /dev/null
+++ b/rdiffweb/tools/real_ip.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# rdiffweb, A web interface to rdiff-backup repositories
+# Copyright (C) 2012-2021 rdiffweb contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import cherrypy
+
+
+def real_ip(remote='X-Real-IP'):
+ """
+ Update the `remote.ip` from the `X-Real-IP` field.
+ """
+
+ request = cherrypy.serving.request
+
+ value = request.headers.get(remote)
+ if value:
+ request.remote.ip = value
+
+
+cherrypy.tools.real_ip = cherrypy.Tool('before_request_body', real_ip, priority=31)