Skip to content

Commit

Permalink
🔒️ Rate limit failed login attempts
Browse files Browse the repository at this point in the history
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.

Cherry picked from 82c892b
  • Loading branch information
foosel committed Aug 16, 2022
1 parent fd2db95 commit a33622e
Show file tree
Hide file tree
Showing 7 changed files with 30 additions and 6 deletions.
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -43,6 +43,7 @@
"Flask-Assets>=2.0,<3",
"Flask-Babel>=2.0,<3",
"Flask-Login>=0.5,<0.6", # flask-login doesn't use semver & breaks stuff on minor version increases
"Flask-Limiter>=2.6,<3",
"flask>=2.1,<2.2", # flask 2.2 requires werkzeug 2.2+
"frozendict>=2.0,<3",
"future>=0.18.2,<1", # not really needed anymore, but leaving in for py2/3 compat plugins
Expand Down
10 changes: 10 additions & 0 deletions src/octoprint/server/__init__.py
Expand Up @@ -70,6 +70,7 @@

assets = None
babel = None
limiter = None
debug = False
safe_mode = False

Expand Down Expand Up @@ -1338,6 +1339,8 @@ def log_heartbeat():
timer.start()

def _setup_app(self, app):
global limiter

from octoprint.server.util.flask import (
OctoPrintFlaskRequest,
OctoPrintFlaskResponse,
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/octoprint/server/api/__init__.py
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/octoprint/static/css/login.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 9 additions & 3 deletions src/octoprint/static/js/login/login.js
Expand Up @@ -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");
Expand All @@ -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)
Expand All @@ -47,7 +49,11 @@ $(function () {
}

overlayElement.removeClass("in");
errorElement.addClass("in");
if (xhr.status === 429) {
errorRateElement.addClass("in");
} else {
errorCredentialsElement.addClass("in");
}
});

return false;
Expand Down
3 changes: 2 additions & 1 deletion src/octoprint/static/less/login.less
Expand Up @@ -31,7 +31,8 @@ body {
}
}

#login-error,
#login-error-credentials,
#login-error-rate,
#login-offline {
display: none;

Expand Down
3 changes: 2 additions & 1 deletion src/octoprint/templates/login.jinja2
Expand Up @@ -65,7 +65,8 @@
<form class="form-signin">
<h2 class="form-signin-heading" data-test-id="login-title">{{ _('Please log in') }}</h2>

<div id="login-error" class="alert alert-error" data-test-id="login-error">{{ _('Incorrect username or password. Hint: Both are case sensitive!') }}</div>
<div id="login-error-credentials" class="alert alert-error" data-test-id="login-error">{{ _('Incorrect username or password. Hint: Both are case sensitive!') }}</div>
<div id="login-error-rate" class="alert alert-error" data-test-id="login-error-rate">{{ _('You have made too many failed login attempts. Please try again later.') }}</div>
<div id="login-offline" class="alert alert-error">{{ _('Server is currently offline.') }} <a id="login-reconnect" href="javascript:void(0)">{{ _('Reconnect...') }}</a></div>

{% if user_id %}<p>
Expand Down

0 comments on commit a33622e

Please sign in to comment.