From 82c892ba40b3741d1b7711d949e56af64f5bc2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 15 Aug 2022 11:21:59 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F=20Rate=20limit=20failed?= =?UTF-8?q?=20login=20attempts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Should reduce likelihood of brute force attacks on unsecure networks if users decide to deploy/make OctoPrint accessible there against all advice to the contrary. --- setup.py | 1 + src/octoprint/server/__init__.py | 10 ++++++++++ src/octoprint/server/api/__init__.py | 5 +++++ src/octoprint/static/css/login.css | 2 +- src/octoprint/static/js/login/login.js | 14 ++++++++++---- src/octoprint/static/less/login.less | 3 ++- src/octoprint/templates/login.jinja2 | 3 ++- 7 files changed, 31 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index 2c0988b310..4aafc687aa 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ "Flask-Assets>=2.0,<3", "Flask-Babel>=2.0,<3", "Flask-Login>=0.6,<0.7", # breaking changes can happen on minor version increases + "Flask-Limiter>=2.6,<3", "flask>=2.2,<2.3", # breaking changes can happen on minor version increases (with deprecation warnings) "frozendict>=2.0,<3", "future>=0.18.2,<1", # not really needed anymore, but leaving in for py2/3 compat plugins diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index eea4b319b6..53b3d6b986 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -70,6 +70,7 @@ assets = None babel = None +limiter = None debug = False safe_mode = False @@ -1338,6 +1339,8 @@ def log_heartbeat(): timer.start() def _setup_app(self, app): + global limiter + from octoprint.server.util.flask import ( OctoPrintFlaskRequest, OctoPrintFlaskResponse, @@ -1433,6 +1436,13 @@ def after_request(response): MarkdownFilter(app) + from flask_limiter import Limiter + from flask_limiter.util import get_remote_address + + app.config["RATELIMIT_STRATEGY"] = "fixed-window-elastic-expiry" + + limiter = Limiter(app, key_func=get_remote_address) + def _setup_i18n(self, app): global babel global LOCALES diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index 83b625e749..08d9ccdd0d 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -281,6 +281,11 @@ def serverStatus(): @api.route("/login", methods=["POST"]) +@octoprint.server.limiter.limit( + "3/minute;5/10 minutes;10/hour", + deduct_when=lambda response: response.status_code == 403, + error_message="You have made too many failed login attempts. Please try again later.", +) def login(): data = request.get_json() if not data: diff --git a/src/octoprint/static/css/login.css b/src/octoprint/static/css/login.css index ad8d238280..ff21ac632b 100644 --- a/src/octoprint/static/css/login.css +++ b/src/octoprint/static/css/login.css @@ -1 +1 @@ -body{padding-top:40px;padding-bottom:40px;background-color:#f5f5f5}.form-signin{max-width:300px;padding:19px 29px 29px;margin:0 auto 20px;background-color:#fff;border:1px solid #e5e5e5;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.form-signin .form-signin-heading,.form-signin .checkbox{margin-bottom:10px}.form-signin input[type="text"],.form-signin input[type="password"]{font-size:16px;height:auto;margin-bottom:15px;padding:7px 9px}#login-error,#login-offline{display:none}#login-error.in,#login-offline.in{display:block}#login-overlay{position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;display:none}#login-overlay.in{display:block}#login-overlay .background{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#000000;filter:alpha(opacity=50);-moz-opacity:.5;-khtml-opacity:.5;opacity:.5}#login-overlay .wrapper{position:absolute;top:0;bottom:0;left:0;right:0}#login-overlay .wrapper .outer{display:table;width:100%;height:100%}#login-overlay .wrapper .outer .inner{display:table-cell;vertical-align:middle}#login-overlay .wrapper .outer .inner .content{text-align:center;color:white}#noscript{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#ffffff;z-index:12310}#noscript .wrapper{position:absolute;top:0;bottom:0;left:0;right:0}#noscript .wrapper .outer{display:table;width:100%;height:100%}#noscript .wrapper .outer .inner{display:table-cell;vertical-align:middle}#noscript .wrapper .outer .inner .content{text-align:center} \ No newline at end of file +body{padding-top:40px;padding-bottom:40px;background-color:#f5f5f5}.form-signin{max-width:300px;padding:19px 29px 29px;margin:0 auto 20px;background-color:#fff;border:1px solid #e5e5e5;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.form-signin .form-signin-heading,.form-signin .checkbox{margin-bottom:10px}.form-signin input[type="text"],.form-signin input[type="password"]{font-size:16px;height:auto;margin-bottom:15px;padding:7px 9px}#login-error-credentials,#login-error-rate,#login-offline{display:none}#login-error-credentials.in,#login-error-rate.in,#login-offline.in{display:block}#login-overlay{position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;display:none}#login-overlay.in{display:block}#login-overlay .background{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#000000;filter:alpha(opacity=50);-moz-opacity:.5;-khtml-opacity:.5;opacity:.5}#login-overlay .wrapper{position:absolute;top:0;bottom:0;left:0;right:0}#login-overlay .wrapper .outer{display:table;width:100%;height:100%}#login-overlay .wrapper .outer .inner{display:table-cell;vertical-align:middle}#login-overlay .wrapper .outer .inner .content{text-align:center;color:white}#noscript{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#ffffff;z-index:12310}#noscript .wrapper{position:absolute;top:0;bottom:0;left:0;right:0}#noscript .wrapper .outer{display:table;width:100%;height:100%}#noscript .wrapper .outer .inner{display:table-cell;vertical-align:middle}#noscript .wrapper .outer .inner .content{text-align:center} \ No newline at end of file diff --git a/src/octoprint/static/js/login/login.js b/src/octoprint/static/js/login/login.js index c9107d2cfb..3256344975 100644 --- a/src/octoprint/static/js/login/login.js +++ b/src/octoprint/static/js/login/login.js @@ -11,7 +11,8 @@ $(function () { }; var overlayElement = $("#login-overlay"); - var errorElement = $("#login-error"); + var errorCredentialsElement = $("#login-error-credentials"); + var errorRateElement = $("#login-error-rate"); var offlineElement = $("#login-offline"); var buttonElement = $("#login-button"); var reconnectElement = $("#login-reconnect"); @@ -28,7 +29,8 @@ $(function () { var remember = rememberElement.prop("checked"); overlayElement.addClass("in"); - errorElement.removeClass("in"); + errorCredentialsElement.removeClass("in"); + errorRateElement.removeClass("in"); OctoPrint.browser .login(username, password, remember) @@ -36,7 +38,7 @@ $(function () { ignoreDisconnect = true; window.location.href = REDIRECT_URL; }) - .fail(() => { + .fail((xhr) => { usernameElement.val(USER_ID); passwordElement.val(""); @@ -47,7 +49,11 @@ $(function () { } overlayElement.removeClass("in"); - errorElement.addClass("in"); + if (xhr.status === 429) { + errorRateElement.addClass("in"); + } else { + errorCredentialsElement.addClass("in"); + } }); return false; diff --git a/src/octoprint/static/less/login.less b/src/octoprint/static/less/login.less index a54d692fc8..8d4f0b8ae5 100644 --- a/src/octoprint/static/less/login.less +++ b/src/octoprint/static/less/login.less @@ -31,7 +31,8 @@ body { } } -#login-error, +#login-error-credentials, +#login-error-rate, #login-offline { display: none; diff --git a/src/octoprint/templates/login.jinja2 b/src/octoprint/templates/login.jinja2 index 252e33dbd7..8162356345 100644 --- a/src/octoprint/templates/login.jinja2 +++ b/src/octoprint/templates/login.jinja2 @@ -65,7 +65,8 @@
-
{{ _('Incorrect username or password. Hint: Both are case sensitive!') }}
+
{{ _('Incorrect username or password. Hint: Both are case sensitive!') }}
+
{{ _('You have made too many failed login attempts. Please try again later.') }}
{{ _('Server is currently offline.') }} {{ _('Reconnect...') }}
{% if user_id %}