From 28258e5dd85e7c417918ed3bf481e19ed5eff91f Mon Sep 17 00:00:00 2001 From: Patrik Dufresne Date: Thu, 15 Sep 2022 15:22:56 -0400 Subject: [PATCH] Use X-Real-IP to identify clients #213 --- README.md | 8 ++++- doc/networking.md | 25 ++++++++----- rdiffweb/controller/tests/test_page_login.py | 30 ++++++++++++++++ rdiffweb/main.py | 3 -- rdiffweb/rdw_app.py | 4 ++- rdiffweb/tools/enrich_session.py | 38 ++++++++++++++++++++ rdiffweb/tools/real_ip.py | 33 +++++++++++++++++ 7 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 rdiffweb/tools/enrich_session.py create mode 100644 rdiffweb/tools/real_ip.py 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)